From 1382f33ae626349b7d8d03f9e5f9555a123078d7 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 30 Sep 2025 15:38:12 +0000 Subject: [PATCH] Added (first version of) sample Hummingbird app. (#4) This PR contains the work done to: * Implemented a basic `Hummingbird` application in which to integrate the `HummingbirdDocC` library. * Added the *ArgumentParser* package dependency to the `Package.swift` file; * Added a new *sample* target to the `Package.swift` file; * Added library and documentation tasks to the `Makefile` file. Reviewed-on: https://repo.rock-n-code.com/rock-n-code/hummingbird-docc/pulls/4 Co-authored-by: Javier Cicchelli Co-committed-by: Javier Cicchelli --- .env | 12 +- Makefile | 46 ++++-- Package.swift | 40 +++-- .../App/Builders/AppBuilder.swift | 86 ++++++++++ .../App/Parameters/SampleAppArguments.swift | 43 +++++ .../App/Protocols/AppArguments.swift | 40 +++++ Samples/HummingbirdDocC/App/SampleApp.swift | 48 ++++++ .../Use Cases/PrepareURIPathUseCase.swift | 127 --------------- .../DoccMiddlewareConfiguration.swift | 52 ------ .../Catalogs/HummingbirdDocC.docc}/Library.md | 2 +- .../Internal/Enumerations/AssetFile.swift | 0 .../Internal/Enumerations/AssetFolder.swift | 0 .../Enumerations/DocumentationFolder.swift | 0 .../Extensions/LoggerMetadata+Helpers.swift | 0 .../Extensions/String+Constants.swift | 0 .../Internal/Extensions/String+Formats.swift | 0 .../Internal/Extensions/String+Helpers.swift | 46 ++++++ .../Internal/Models/Resource.swift | 50 ++++++ .../Internal/Protocols/Pathable.swift | 0 .../Pseudo Types/ContextualInfo.swift | 0 .../Internal/Use Cases/CheckURIUseCase.swift | 20 ++- .../Use Cases/PrepareURIPathUseCase.swift | 124 ++++++++++++++ .../Use Cases/RedirectURIUseCase.swift | 0 .../Internal/Use Cases/ServeURIUseCase.swift | 0 .../Configurations/DoccConfiguration.swift | 50 ++++++ .../Public/Middlewares/DocCMiddleware.swift | 152 +++++++++--------- .../Use Cases/CheckURIUseCaseTests.swift | 116 ------------- .../Enumerations/AssetFileTests.swift | 2 +- .../Enumerations/AssetFolderTests.swift | 2 +- .../DocumentationFolderTests.swift | 2 +- .../LoggerMetadata+HelpersTests.swift | 2 +- .../Extensions/String_HelpersTests.swift | 97 +++++++++++ .../Tests/Internal/Models/ResourceTests.swift | 106 ++++++++++++ .../Use Cases/CheckURIUseCaseTests.swift | 147 +++++++++++++++++ .../PrepareURIPathUseCaseTests.swift | 50 +++--- .../Use Cases/RedirectURIUseCaseTests.swift | 4 +- .../Use Cases/ServeURIUseCaseTests.swift | 4 +- .../Middlewares/DocCMiddlewareTests.swift | 67 ++++---- .../Types/Extensions/Logger+Helpers.swift | 2 +- .../Extensions/LoggerLevel+Helpers.swift | 0 .../Types/Extensions/Request+Helpers.swift | 0 .../Types/Extensions/String+Constants.swift | 0 .../Types/Extensions/Tag+Constants.swift | 2 + .../Types/Mocks/FileProviderMock.swift | 0 .../Types/Mocks/LogHandlerMock.swift | 42 +++-- .../Types/Mocks/RequestContextMock.swift | 0 .../Types/Namespaces/Input.swift | 0 .../Types/Namespaces/Output.swift | 0 .../Types/Stubs/FileProviderStub.swift | 0 49 files changed, 1095 insertions(+), 488 deletions(-) create mode 100644 Samples/HummingbirdDocC/App/Builders/AppBuilder.swift create mode 100644 Samples/HummingbirdDocC/App/Parameters/SampleAppArguments.swift create mode 100644 Samples/HummingbirdDocC/App/Protocols/AppArguments.swift create mode 100644 Samples/HummingbirdDocC/App/SampleApp.swift delete mode 100644 Sources/DocCMiddleware/Internal/Use Cases/PrepareURIPathUseCase.swift delete mode 100644 Sources/DocCMiddleware/Public/Configurations/DoccMiddlewareConfiguration.swift rename Sources/{DocCMiddleware/Catalogs/DocCMiddleware.docc => HummingbirdDocC/Catalogs/HummingbirdDocC.docc}/Library.md (92%) rename Sources/{DocCMiddleware => HummingbirdDocC}/Internal/Enumerations/AssetFile.swift (100%) rename Sources/{DocCMiddleware => HummingbirdDocC}/Internal/Enumerations/AssetFolder.swift (100%) rename Sources/{DocCMiddleware => HummingbirdDocC}/Internal/Enumerations/DocumentationFolder.swift (100%) rename Sources/{DocCMiddleware => HummingbirdDocC}/Internal/Extensions/LoggerMetadata+Helpers.swift (100%) rename Sources/{DocCMiddleware => HummingbirdDocC}/Internal/Extensions/String+Constants.swift (100%) rename Sources/{DocCMiddleware => HummingbirdDocC}/Internal/Extensions/String+Formats.swift (100%) create mode 100644 Sources/HummingbirdDocC/Internal/Extensions/String+Helpers.swift create mode 100644 Sources/HummingbirdDocC/Internal/Models/Resource.swift rename Sources/{DocCMiddleware => HummingbirdDocC}/Internal/Protocols/Pathable.swift (100%) rename Sources/{DocCMiddleware => HummingbirdDocC}/Internal/Pseudo Types/ContextualInfo.swift (100%) rename Sources/{DocCMiddleware => HummingbirdDocC}/Internal/Use Cases/CheckURIUseCase.swift (67%) create mode 100644 Sources/HummingbirdDocC/Internal/Use Cases/PrepareURIPathUseCase.swift rename Sources/{DocCMiddleware => HummingbirdDocC}/Internal/Use Cases/RedirectURIUseCase.swift (100%) rename Sources/{DocCMiddleware => HummingbirdDocC}/Internal/Use Cases/ServeURIUseCase.swift (100%) create mode 100644 Sources/HummingbirdDocC/Public/Configurations/DoccConfiguration.swift rename Sources/{DocCMiddleware => HummingbirdDocC}/Public/Middlewares/DocCMiddleware.swift (69%) delete mode 100644 Tests/DocCMiddleware/Tests/Internal/Use Cases/CheckURIUseCaseTests.swift rename Tests/{DocCMiddleware => HummingbirdDocC}/Tests/Internal/Enumerations/AssetFileTests.swift (97%) rename Tests/{DocCMiddleware => HummingbirdDocC}/Tests/Internal/Enumerations/AssetFolderTests.swift (97%) rename Tests/{DocCMiddleware => HummingbirdDocC}/Tests/Internal/Enumerations/DocumentationFolderTests.swift (97%) rename Tests/{DocCMiddleware => HummingbirdDocC}/Tests/Internal/Extensions/LoggerMetadata+HelpersTests.swift (99%) create mode 100644 Tests/HummingbirdDocC/Tests/Internal/Extensions/String_HelpersTests.swift create mode 100644 Tests/HummingbirdDocC/Tests/Internal/Models/ResourceTests.swift create mode 100644 Tests/HummingbirdDocC/Tests/Internal/Use Cases/CheckURIUseCaseTests.swift rename Tests/{DocCMiddleware => HummingbirdDocC}/Tests/Internal/Use Cases/PrepareURIPathUseCaseTests.swift (69%) rename Tests/{DocCMiddleware => HummingbirdDocC}/Tests/Internal/Use Cases/RedirectURIUseCaseTests.swift (97%) rename Tests/{DocCMiddleware => HummingbirdDocC}/Tests/Internal/Use Cases/ServeURIUseCaseTests.swift (98%) rename Tests/{DocCMiddleware => HummingbirdDocC}/Tests/Public/Middlewares/DocCMiddlewareTests.swift (90%) rename Tests/{DocCMiddleware => HummingbirdDocC}/Types/Extensions/Logger+Helpers.swift (95%) rename Tests/{DocCMiddleware => HummingbirdDocC}/Types/Extensions/LoggerLevel+Helpers.swift (100%) rename Tests/{DocCMiddleware => HummingbirdDocC}/Types/Extensions/Request+Helpers.swift (100%) rename Tests/{DocCMiddleware => HummingbirdDocC}/Types/Extensions/String+Constants.swift (100%) rename Tests/{DocCMiddleware => HummingbirdDocC}/Types/Extensions/Tag+Constants.swift (91%) rename Tests/{DocCMiddleware => HummingbirdDocC}/Types/Mocks/FileProviderMock.swift (100%) rename Tests/{DocCMiddleware => HummingbirdDocC}/Types/Mocks/LogHandlerMock.swift (75%) rename Tests/{DocCMiddleware => HummingbirdDocC}/Types/Mocks/RequestContextMock.swift (100%) rename Tests/{DocCMiddleware => HummingbirdDocC}/Types/Namespaces/Input.swift (100%) rename Tests/{DocCMiddleware => HummingbirdDocC}/Types/Namespaces/Output.swift (100%) rename Tests/{DocCMiddleware => HummingbirdDocC}/Types/Stubs/FileProviderStub.swift (100%) diff --git a/.env b/.env index 1dde1ce..8916f17 100644 --- a/.env +++ b/.env @@ -12,11 +12,15 @@ # --- DOCUMENTATION --- +DOCC_ARCHIVE_BASE_PATH=archives/${SPM_LIBRARY_TARGET} +DOCC_ARCHIVE_OUTPUT=./${SPM_LIBRARY_TARGET}.doccarchive +DOCC_ARCHIVE_REFERENCE=hummingbirddocc +DOCC_CONFIG_MINIMUM_ACCESS_LEVEL=public +DOCC_CONFIG_PREVIEW_URL=http://localhost:8080/documentation/${DOCC_ARCHIVE_REFERENCE} +DOCC_GITHUB_BASE_PATH=hummingbird-docc DOCC_GITHUB_OUTPUT=./docs -DOCC_GITHUB_BASE_PATH=hummingbird-docc-middleware -DOCC_PREVIEW_URL=http://localhost:8080/documentation/doccmiddleware -DOCC_XCODE_OUTPUT=./${SPM_LIBRARY_TARGET}.doccarchive # -- SWIFT PACKAGE MANAGER --- -SPM_LIBRARY_TARGET=DocCMiddleware \ No newline at end of file +SPM_LIBRARY_TARGET=HummingbirdDocC +SPM_SAMPLE_TARGET=HummingbirdDocCSample \ No newline at end of file diff --git a/Makefile b/Makefile index e11e383..7180b6d 100644 --- a/Makefile +++ b/Makefile @@ -20,10 +20,18 @@ export $(shell sed 's/=.*//' $(environment)) # LIBRARY lib-build: ## Builds the library - @swift build + @swift build \ + --target $(SPM_LIBRARY_TARGET) lib-release: ## Releases the library - @swift build -c release + @swift build \ + --target $(SPM_LIBRARY_TARGET) \ + --configuration release + +lib-sample: ## Runs the sample app of the library + @swift run \ + --configuration release \ + --disable-sandbox lib-test: ## Runs the unit tests for the library @swift test \ @@ -41,8 +49,8 @@ pkg-reset: ## Resets the complete SPM cache/build folder of the package @swift package reset pkg-pristine: pkg-clean pkg-reset ## Deletes all built artifacts, caches, and documentations of the package + @rm -drf $(DOCC_ARCHIVE_OUTPUT) @rm -drf $(DOCC_GITHUB_OUTPUT) - @rm -drf $(DOCC_XCODE_OUTPUT) pkg-outdated: ## Lists the SPM package dependencies that can be updated @swift package update --dry-run @@ -52,7 +60,18 @@ pkg-update: ## Updates the SPM package dependencies # DOCUMENTATION -doc-generate: doc-generate-xcode doc-generate-github ## Generates the library documentation for both Github and Xcode +doc-generate: doc-generate-archive doc-generate-github ## Generates the library documentation for both Github and Xcode + +doc-generate-archive: ## Generates the library documentation archive for Xcode + @swift package \ + --allow-writing-to-directory $(DOCC_ARCHIVE_OUTPUT) \ + generate-documentation \ + --target $(SPM_LIBRARY_TARGET) \ + --hosting-base-path $(DOCC_ARCHIVE_BASE_PATH) \ + --output-path $(DOCC_ARCHIVE_OUTPUT) \ + --symbol-graph-minimum-access-level $(DOCC_CONFIG_MINIMUM_ACCESS_LEVEL) \ + --include-extended-types \ + --enable-inherited-docs doc-generate-github: ## Generates the library documentation for Github @swift package \ @@ -62,21 +81,20 @@ doc-generate-github: ## Generates the library documentation for Github --disable-indexing \ --transform-for-static-hosting \ --hosting-base-path $(DOCC_GITHUB_BASE_PATH) \ - --output-path $(DOCC_GITHUB_OUTPUT) - -doc-generate-xcode: ## Generates the library documentation for Xcode - @swift package \ - --allow-writing-to-directory $(DOCC_XCODE_OUTPUT) \ - generate-documentation \ - --target $(SPM_LIBRARY_TARGET) \ - --output-path $(DOCC_XCODE_OUTPUT) + --output-path $(DOCC_GITHUB_OUTPUT) \ + --symbol-graph-minimum-access-level $(DOCC_CONFIG_MINIMUM_ACCESS_LEVEL) \ + --include-extended-types \ + --enable-inherited-docs doc-preview: ## Previews the library documentation in Safari - @open -a safari $(DOCC_PREVIEW_URL) + @open -a safari $(DOCC_CONFIG_PREVIEW_URL) @swift package \ --disable-sandbox \ preview-documentation \ - --target $(SPM_LIBRARY_TARGET) + --target $(SPM_LIBRARY_TARGET) \ + --symbol-graph-minimum-access-level $(DOCC_CONFIG_MINIMUM_ACCESS_LEVEL) \ + --include-extended-types \ + --enable-inherited-docs # IDE diff --git a/Package.swift b/Package.swift index ee93630..ed22d10 100644 --- a/Package.swift +++ b/Package.swift @@ -3,7 +3,7 @@ import PackageDescription let package = Package( - name: DocCMiddleware.package, + name: HummingbirdDocC.package, platforms: [ .iOS(.v17), .macCatalyst(.v17), @@ -13,38 +13,54 @@ let package = Package( ], products: [ .library( - name: DocCMiddleware.package, - targets: [DocCMiddleware.target] + name: HummingbirdDocC.package, + targets: [HummingbirdDocC.target] ), ], dependencies: [ + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.4.0"), .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0"), .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0"), ], targets: [ + .executableTarget( + name: HummingbirdDocC.sample, + dependencies: [ + .byName(name: HummingbirdDocC.target), + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ], + path: "Samples/HummingbirdDocC", + swiftSettings: [ + // Enable better optimizations when building in Release configuration. Despite the use of + // the `.unsafeFlags` construct required by SwiftPM, this flag is recommended for Release + // builds. See for details. + .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release)), + ] + ), .target( - name: DocCMiddleware.target, + name: HummingbirdDocC.target, dependencies: [ .product(name: "Hummingbird", package: "hummingbird"), ], - path: "Sources/DocCMiddleware", + path: "Sources/HummingbirdDocC", swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")] ), .testTarget( - name: DocCMiddleware.test, + name: HummingbirdDocC.test, dependencies: [ .product(name: "HummingbirdTesting", package: "hummingbird"), - .byName(name: DocCMiddleware.target) + .byName(name: HummingbirdDocC.target) ], - path: "Tests/DocCMiddleware" + path: "Tests/HummingbirdDocC" ), ] ) // MARK: - Constants -enum DocCMiddleware { - static let package = "hummingbird-docc-middleware" - static let target = "DocCMiddleware" - static let test = "\(DocCMiddleware.target)Tests" +enum HummingbirdDocC { + static let package = "hummingbird-docc" + static let sample = "\(HummingbirdDocC.target)Sample" + static let target = "HummingbirdDocC" + static let test = "\(HummingbirdDocC.target)Test" } diff --git a/Samples/HummingbirdDocC/App/Builders/AppBuilder.swift b/Samples/HummingbirdDocC/App/Builders/AppBuilder.swift new file mode 100644 index 0000000..9fb1639 --- /dev/null +++ b/Samples/HummingbirdDocC/App/Builders/AppBuilder.swift @@ -0,0 +1,86 @@ +// ===----------------------------------------------------------------------=== +// +// 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 Hummingbird.Router + +import protocol Hummingbird.ApplicationProtocol + +import struct Hummingbird.Application +import struct Hummingbird.BasicRequestContext +import struct Hummingbird.BindAddress +import struct Hummingbird.LogRequestsMiddleware +import struct HummingbirdDocC.DocCConfiguration +import struct HummingbirdDocC.DocCMiddleware +import struct Logging.Logger + +struct AppBuilder { + + // MARK: Properties + + /// A type that interacts with the logging system. + private let logger: Logger + + // MARK: Initializers + + /// Initializes this builder. + /// - Parameter logger: A type that interacts with the logging system. + init(logger: Logger) { + self.logger = logger + } + + // MARK: Functions + + func callAsFunction( + _ arguments: AppArguments + ) -> some ApplicationProtocol { + return Application( + router: router(), + configuration: .init( + address: .hostname( + arguments.hostname, + port: arguments.port + ) + ), + logger: logger + ) + } + +} + +// MARK: - Helpers + +private extension AppBuilder { + + // MARK: Type aliases + + typealias AppRequestContext = BasicRequestContext + + // MARK: Functions + + func router() -> Router { + let router = Router() + + router.addMiddleware { + LogRequestsMiddleware(logger.logLevel) + DocCMiddleware ( + configuration: DocCConfiguration( + uriRoot: "/archives", + folderRoot: "Samples/HummingbirdDocC/Archives" + ), + logger: logger + ) + } + + return router + } + +} diff --git a/Samples/HummingbirdDocC/App/Parameters/SampleAppArguments.swift b/Samples/HummingbirdDocC/App/Parameters/SampleAppArguments.swift new file mode 100644 index 0000000..f0ecde9 --- /dev/null +++ b/Samples/HummingbirdDocC/App/Parameters/SampleAppArguments.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 +// +// ===----------------------------------------------------------------------=== + +import protocol ArgumentParser.ParsableArguments + +import struct ArgumentParser.Option +import struct Logging.Logger + +extension SampleApp { + /// A type that conforms to the ``AppArguments`` and the `ParsableArguments` protocols, which contains the input parameters required for the + /// execution of the sample executable. + struct Arguments: AppArguments, ParsableArguments { + + // MARK: Properties + + @Option( + name: .shortAndLong, + help: "A label given to the sample app for the sole purpose of identification within a communications channel." + ) + var hostname: String = "127.0.0.1" + + @Option( + name: .shortAndLong, + help: "A port number assigned to the sample app from where the app either sends or receives data." + ) + var port: Int = 8080 + + @Option( + name: .long, + help: "A log level to configure in a type that interacts with the logging system." + ) + var logLevel: Logger.Level = .trace + } +} diff --git a/Samples/HummingbirdDocC/App/Protocols/AppArguments.swift b/Samples/HummingbirdDocC/App/Protocols/AppArguments.swift new file mode 100644 index 0000000..7fbb496 --- /dev/null +++ b/Samples/HummingbirdDocC/App/Protocols/AppArguments.swift @@ -0,0 +1,40 @@ +// ===----------------------------------------------------------------------=== +// +// 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 ArgumentParser.ExpressibleByArgument + +import struct Logging.Logger + +/// A protocol that defines the input arguments the sample executable requires to run. +protocol AppArguments { + + // MARK: Properties + + /// A label given to the sample app to identify it within a communications channel. + var hostname: String { get } + + /// A port number assigned to the sample app from where the app either sends or receives data. + var port: Int { get } + + /// A log level to configure in a type that interacts with the logging system. + var logLevel: Logger.Level { get } + +} + +// MARK: - Conformances + +/// Extends the `Logger.Level` type so it can be used as an argument. +#if hasFeature(RetroactiveAttribute) +extension Logger.Level: @retroactive ExpressibleByArgument {} +#else +extension Logger.Level: ExpressibleByArgument {} +#endif diff --git a/Samples/HummingbirdDocC/App/SampleApp.swift b/Samples/HummingbirdDocC/App/SampleApp.swift new file mode 100644 index 0000000..5d35bc7 --- /dev/null +++ b/Samples/HummingbirdDocC/App/SampleApp.swift @@ -0,0 +1,48 @@ +// ===----------------------------------------------------------------------=== +// +// 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 ArgumentParser.AsyncParsableCommand + +import struct ArgumentParser.OptionGroup +import struct Logging.Logger + +/// A type that implements and runs the sample executable that showcases the `Hummingbird-DocC` middleware. +@main struct SampleApp { + + // MARK: Properties + + /// A type that contains all the necessary input parameters to run the sample executable. + @OptionGroup var arguments: Arguments + +} + +// MARK: - AsyncParsableCommand + +extension SampleApp: AsyncParsableCommand { + + // MARK: Functions + + func run() async throws { + let builder = AppBuilder(logger: { + var logger = Logger(label: "sample.hummingbird-docc.logger") + + logger.logLevel = arguments.logLevel + + return logger + }()) + + let application = builder(arguments) + + try await application.run() + } + +} diff --git a/Sources/DocCMiddleware/Internal/Use Cases/PrepareURIPathUseCase.swift b/Sources/DocCMiddleware/Internal/Use Cases/PrepareURIPathUseCase.swift deleted file mode 100644 index 9611a9b..0000000 --- a/Sources/DocCMiddleware/Internal/Use Cases/PrepareURIPathUseCase.swift +++ /dev/null @@ -1,127 +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 RegexBuilder - -/// A use case that extracts data from a given URI path, essential for routing the documentation contents. -struct PrepareURIPathUseCase { - - // MARK: Type aliases - - /// 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 - - /// 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 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, reference and URI path, plus the resource URI paths. - func callAsFunction(_ uriPath: String) -> PreparedURIPaths? { - guard let uriRest = restOfURIPath(from: uriPath) else { - return nil - } - - let archiveName = uriRest - .split(separator: .Path.forwardSlash) - .map(String.init) - .first - - let archiveReference: String = if let archiveName { - archiveName.lowercased() - } else { - .empty - } - let archivePath: String = if let archiveName { - .init(format: .Format.Path.archive, archiveName) - } else { - .empty - } - - return ( - archiveName ?? .empty, - archiveReference, - 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 .Path.forwardSlash - } - guard uriRest.hasPrefix(.Path.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 deleted file mode 100644 index 0bc36c4..0000000 --- a/Sources/DocCMiddleware/Public/Configurations/DoccMiddlewareConfiguration.swift +++ /dev/null @@ -1,52 +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 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 - } - - } -} diff --git a/Sources/DocCMiddleware/Catalogs/DocCMiddleware.docc/Library.md b/Sources/HummingbirdDocC/Catalogs/HummingbirdDocC.docc/Library.md similarity index 92% rename from Sources/DocCMiddleware/Catalogs/DocCMiddleware.docc/Library.md rename to Sources/HummingbirdDocC/Catalogs/HummingbirdDocC.docc/Library.md index 83274a1..337e27f 100644 --- a/Sources/DocCMiddleware/Catalogs/DocCMiddleware.docc/Library.md +++ b/Sources/HummingbirdDocC/Catalogs/HummingbirdDocC.docc/Library.md @@ -1,4 +1,4 @@ -# ``DocCMiddleware`` +# ``HummingbirdDocC`` Summary diff --git a/Sources/DocCMiddleware/Internal/Enumerations/AssetFile.swift b/Sources/HummingbirdDocC/Internal/Enumerations/AssetFile.swift similarity index 100% rename from Sources/DocCMiddleware/Internal/Enumerations/AssetFile.swift rename to Sources/HummingbirdDocC/Internal/Enumerations/AssetFile.swift diff --git a/Sources/DocCMiddleware/Internal/Enumerations/AssetFolder.swift b/Sources/HummingbirdDocC/Internal/Enumerations/AssetFolder.swift similarity index 100% rename from Sources/DocCMiddleware/Internal/Enumerations/AssetFolder.swift rename to Sources/HummingbirdDocC/Internal/Enumerations/AssetFolder.swift diff --git a/Sources/DocCMiddleware/Internal/Enumerations/DocumentationFolder.swift b/Sources/HummingbirdDocC/Internal/Enumerations/DocumentationFolder.swift similarity index 100% rename from Sources/DocCMiddleware/Internal/Enumerations/DocumentationFolder.swift rename to Sources/HummingbirdDocC/Internal/Enumerations/DocumentationFolder.swift diff --git a/Sources/DocCMiddleware/Internal/Extensions/LoggerMetadata+Helpers.swift b/Sources/HummingbirdDocC/Internal/Extensions/LoggerMetadata+Helpers.swift similarity index 100% rename from Sources/DocCMiddleware/Internal/Extensions/LoggerMetadata+Helpers.swift rename to Sources/HummingbirdDocC/Internal/Extensions/LoggerMetadata+Helpers.swift diff --git a/Sources/DocCMiddleware/Internal/Extensions/String+Constants.swift b/Sources/HummingbirdDocC/Internal/Extensions/String+Constants.swift similarity index 100% rename from Sources/DocCMiddleware/Internal/Extensions/String+Constants.swift rename to Sources/HummingbirdDocC/Internal/Extensions/String+Constants.swift diff --git a/Sources/DocCMiddleware/Internal/Extensions/String+Formats.swift b/Sources/HummingbirdDocC/Internal/Extensions/String+Formats.swift similarity index 100% rename from Sources/DocCMiddleware/Internal/Extensions/String+Formats.swift rename to Sources/HummingbirdDocC/Internal/Extensions/String+Formats.swift diff --git a/Sources/HummingbirdDocC/Internal/Extensions/String+Helpers.swift b/Sources/HummingbirdDocC/Internal/Extensions/String+Helpers.swift new file mode 100644 index 0000000..dc00ff3 --- /dev/null +++ b/Sources/HummingbirdDocC/Internal/Extensions/String+Helpers.swift @@ -0,0 +1,46 @@ +// ===----------------------------------------------------------------------=== +// +// 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 RegexBuilder + +extension String { + + /// Subtracts a prefix from a string. + /// - Parameters: + /// - prefix: A prefix to remove from a string. + /// - Returns: A new string with a prefix removed, if any. + func subtract( + _ prefix: String + ) -> String? { + let reference = Reference(String.self) + let pattern = Regex { + prefix + Optionally { + Capture(as: reference) { + OneOrMore(.anyNonNewline) + } transform: { output in + String(output) + } + } + } + + guard let matches = self.prefixMatch(of: pattern) else { + return nil + } + guard let subtracted = matches.output.1 else { + return .empty + } + + return subtracted + } + +} diff --git a/Sources/HummingbirdDocC/Internal/Models/Resource.swift b/Sources/HummingbirdDocC/Internal/Models/Resource.swift new file mode 100644 index 0000000..db07960 --- /dev/null +++ b/Sources/HummingbirdDocC/Internal/Models/Resource.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 +// +// ===----------------------------------------------------------------------=== + +/// A model that encapsulates the information related to a resource in a given `DocC` documentation archive. +struct Resource: Equatable { + + // MARK: Properties + + /// An archive name in which the resource belongs to. + let archiveName: String + + /// A relative URI path to the resource. + let relativePath: String + + // MARK: Initializers + + /// Initializes this resource. + /// - Parameters: + /// - archiveName: An archive name in which the resource belongs to. + /// - relativePath: A relative URI path to the resource. + init( + archiveName: String, + relativePath: String + ) { + self.archiveName = archiveName + self.relativePath = relativePath + } + + // MARK: Computed + + /// A relative URI path to a documentation archive the resource belongs to. + var archivePath: String { + .init(format: .Format.Path.archive, archiveName) + } + + /// A reference name for the documentation archive the resource belongs to. + var archiveReference: String { + archiveName.lowercased() + } + +} diff --git a/Sources/DocCMiddleware/Internal/Protocols/Pathable.swift b/Sources/HummingbirdDocC/Internal/Protocols/Pathable.swift similarity index 100% rename from Sources/DocCMiddleware/Internal/Protocols/Pathable.swift rename to Sources/HummingbirdDocC/Internal/Protocols/Pathable.swift diff --git a/Sources/DocCMiddleware/Internal/Pseudo Types/ContextualInfo.swift b/Sources/HummingbirdDocC/Internal/Pseudo Types/ContextualInfo.swift similarity index 100% rename from Sources/DocCMiddleware/Internal/Pseudo Types/ContextualInfo.swift rename to Sources/HummingbirdDocC/Internal/Pseudo Types/ContextualInfo.swift diff --git a/Sources/DocCMiddleware/Internal/Use Cases/CheckURIUseCase.swift b/Sources/HummingbirdDocC/Internal/Use Cases/CheckURIUseCase.swift similarity index 67% rename from Sources/DocCMiddleware/Internal/Use Cases/CheckURIUseCase.swift rename to Sources/HummingbirdDocC/Internal/Use Cases/CheckURIUseCase.swift index 81ddf5e..6c72f94 100644 --- a/Sources/DocCMiddleware/Internal/Use Cases/CheckURIUseCase.swift +++ b/Sources/HummingbirdDocC/Internal/Use Cases/CheckURIUseCase.swift @@ -14,6 +14,22 @@ 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: Properties + + /// A root path that prefixes 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 @@ -23,8 +39,8 @@ struct CheckURIUseCase { func callAsFunction(_ uri: URI) -> String? { guard let uriPath = uri.path.removingPercentEncoding, - !uriPath.contains(.Path.previousFolder), - uriPath.hasPrefix(.Path.forwardSlash) + uriPath.hasPrefix(uriRoot), + !uriPath.contains(.Path.previousFolder) else { return nil } diff --git a/Sources/HummingbirdDocC/Internal/Use Cases/PrepareURIPathUseCase.swift b/Sources/HummingbirdDocC/Internal/Use Cases/PrepareURIPathUseCase.swift new file mode 100644 index 0000000..d5f9b38 --- /dev/null +++ b/Sources/HummingbirdDocC/Internal/Use Cases/PrepareURIPathUseCase.swift @@ -0,0 +1,124 @@ +// ===----------------------------------------------------------------------=== +// +// 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 extracts a resource's information from a given URI path, essential for routing the documentation contents. +struct PrepareURIPathUseCase { + + // MARK: Properties + + /// A root path that prefixes 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 resource's information that is essential for documentation contents routing from a given 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 resource type containing all the relevant information + func callAsFunction(_ uriPath: String) -> Resource? { + guard let uriRest = uriRest(from: uriPath) else { + return nil + } + + let archiveName = archiveName(from: uriRest) + + guard let relativePath = relativePath(from: uriRest, at: archiveName) else { + return nil + } + + return .init( + archiveName: archiveName, + relativePath: relativePath + ) + } + +} + +// MARK: - Helpers + +private extension PrepareURIPathUseCase { + + // MARK: Functions + + /// Extracts the archive name from a given URI path. + /// + /// > important: It is assumed that a given URI path is a relative URI path is prefixed with the `/` character and that contains the name of a `DocC` + /// documentation archive in its first component. + /// + /// - Parameter uriPath: A relative URI path to extract the archive name from. + /// - Returns: An archive named extracted from a given URI path. + func archiveName(from uriPath: String) -> String { + uriPath + .split(separator: .Path.forwardSlash) + .map(String.init) + .first ?? .empty + } + + /// Extracts the rest of the URI path from a given URI path against a defined URI root path. + /// - 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 uriRest(from uriPath: String) -> String? { + guard let uriRest = uriPath.subtract(uriRoot) else { + return nil + } + guard !uriRest.isEmpty else { + return uriRest + } + guard uriRest.hasPrefix(.Path.forwardSlash) else { + return .init(format: .Format.Path.root, uriRest) + } + + return uriRest + } + + /// Extracts the relative URI path from a given URI path against the name of a documentation archive. + /// - Parameters: + /// - uriPath: A URI path to get the relative URI path from. + /// - archive: An archive name to subtract from a URI path. + /// - Returns: A relative 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 relativePath( + from uriPath: String, + at archive: String + ) -> String? { + guard !archive.isEmpty else { + return .Path.forwardSlash + } + + let archivePath: String = .init(format: .Format.Path.root, archive) + + guard let relativePath = uriPath.subtract(archivePath) else { + return nil + } + guard relativePath != .empty else { + return .empty + } + + return relativePath + } + +} diff --git a/Sources/DocCMiddleware/Internal/Use Cases/RedirectURIUseCase.swift b/Sources/HummingbirdDocC/Internal/Use Cases/RedirectURIUseCase.swift similarity index 100% rename from Sources/DocCMiddleware/Internal/Use Cases/RedirectURIUseCase.swift rename to Sources/HummingbirdDocC/Internal/Use Cases/RedirectURIUseCase.swift diff --git a/Sources/DocCMiddleware/Internal/Use Cases/ServeURIUseCase.swift b/Sources/HummingbirdDocC/Internal/Use Cases/ServeURIUseCase.swift similarity index 100% rename from Sources/DocCMiddleware/Internal/Use Cases/ServeURIUseCase.swift rename to Sources/HummingbirdDocC/Internal/Use Cases/ServeURIUseCase.swift diff --git a/Sources/HummingbirdDocC/Public/Configurations/DoccConfiguration.swift b/Sources/HummingbirdDocC/Public/Configurations/DoccConfiguration.swift new file mode 100644 index 0000000..760b784 --- /dev/null +++ b/Sources/HummingbirdDocC/Public/Configurations/DoccConfiguration.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 class NIOPosix.NIOThreadPool + +/// A type that contains all the parameters to configure the ``DocCMiddleware`` middleware. +public struct DocCConfiguration: 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 + } + +} diff --git a/Sources/DocCMiddleware/Public/Middlewares/DocCMiddleware.swift b/Sources/HummingbirdDocC/Public/Middlewares/DocCMiddleware.swift similarity index 69% rename from Sources/DocCMiddleware/Public/Middlewares/DocCMiddleware.swift rename to Sources/HummingbirdDocC/Public/Middlewares/DocCMiddleware.swift index 2f33c20..67712da 100644 --- a/Sources/DocCMiddleware/Public/Middlewares/DocCMiddleware.swift +++ b/Sources/HummingbirdDocC/Public/Middlewares/DocCMiddleware.swift @@ -23,32 +23,31 @@ import struct Logging.Logger /// /// This middleware routes the contents of a `DocC` documentation container, defined by its resource URI paths, following these rules: /// -/// 1. *Redirects the URI path `/` to the path `//`*; -/// 2. *Redirects the URI path `//` to the path `//documentation`* -/// 3. *Redirects the URI path `//documentation` to the path `//documentation/`* -/// 4. *Redirects the URI path `//tutorials` to the path `//tutorials/`* -/// 5. *Redirects the URI path `//documentation/` to the resource on `/.doccarchive/documentation//index.html`* -/// 6. *Redirects the URI path `//tutorials/` to the resource on `/.doccarchive/tutorials//index.html`* -/// 7. *Redirects the URI path `//data/documentation.json` to the resource on `/.doccarchive/data/documentation/.json`* -/// 8. *Redirects the URI path `//favicon.ico` to the resource on `/.doccarchive/favicon.ico`* -/// 9. *Redirects the URI path `//favicon.svg` to the resource on `/.doccarchive/favicon.svg`* -/// 10. *Redirects the URI path `//theme-settings.json` to the resource on `/.doccarchive/theme-settings.json`* -/// 11. *Redirects the URI path `//css/` to the resource on `/.doccarchive/css/`* -/// 12. *Redirects the URI path `//data/` to the resource on `/.doccarchive/data/`* -/// 13. *Redirects the URI path `//downloads/` to the resource on `/.doccarchive/downloads/`* -/// 14. *Redirects the URI path `//images/` to the resource on `/.doccarchive/images/`* -/// 15. *Redirects the URI path `//img/` to the resource on `/.doccarchive/img/`* -/// 16. *Redirects the URI path `//index/` to the resource on `/.doccarchive/index/`* -/// 17. *Redirects the URI path `//js/` to the resource on `/.doccarchive/js/`* -/// 18. *Redirects the URI path `//videos/` to the resource on `/.doccarchive/videos/`* +/// 1. *Redirects the URI path `/` or `/` to the path `//`*; +/// 2. *Redirects the URI path `//documentation` to the path `//documentation/`* +/// 3. *Redirects the URI path `//tutorials` to the path `//tutorials/`* +/// 4. *Redirects the URI path `//documentation/` to the resource on `/.doccarchive/documentation//index.html`* +/// 5. *Redirects the URI path `//tutorials/` to the resource on `/.doccarchive/tutorials//index.html`* +/// 6. *Redirects the URI path `//data/documentation.json` to the resource on `/.doccarchive/data/documentation/.json`* +/// 7. *Redirects the URI path `//favicon.ico` to the resource on `/.doccarchive/favicon.ico`* +/// 8. *Redirects the URI path `//favicon.svg` to the resource on `/.doccarchive/favicon.svg`* +/// 9. *Redirects the URI path `//theme-settings.json` to the resource on `/.doccarchive/theme-settings.json`* +/// 10. *Redirects the URI path `//css/` to the resource on `/.doccarchive/css/`* +/// 11. *Redirects the URI path `//data/` to the resource on `/.doccarchive/data/`* +/// 12. *Redirects the URI path `//downloads/` to the resource on `/.doccarchive/downloads/`* +/// 13. *Redirects the URI path `//images/` to the resource on `/.doccarchive/images/`* +/// 14. *Redirects the URI path `//img/` to the resource on `/.doccarchive/img/`* +/// 15. *Redirects the URI path `//index/` to the resource on `/.doccarchive/index/`* +/// 16. *Redirects the URI path `//js/` to the resource on `/.doccarchive/js/`* +/// 17. *Redirects the URI path `//videos/` to the resource on `/.doccarchive/videos/`* /// -public struct DocCMiddleware { +public struct DocCMiddleware< + Context: RequestContext, + FileSystemProvider: FileProvider +> { // MARK: Properties - /// A type that contains the parameters to configure the middleware. - let configuration: Configuration - /// A type that conforms to a protocol that defines file system interactions. let fileProvider: FileSystemProvider @@ -56,7 +55,7 @@ public struct DocCMiddleware { let logger: Logger /// A use case that checks whether a received URI could be processed or not. - private let checkURI: CheckURIUseCase = .init() + private let checkURI: CheckURIUseCase /// A use case that extracts data from a given URI path, essential for routing the documentation contents. private let prepareURIPath: PrepareURIPathUseCase @@ -74,7 +73,7 @@ public struct DocCMiddleware { /// - configuration: A type that contains the parameters to configure the middleware. /// - logger: A type that interacts with the logging system. public init( - configuration: Configuration, + configuration: DocCConfiguration, logger: Logger ) where FileSystemProvider == LocalFileSystem { self.init( @@ -94,13 +93,13 @@ public struct DocCMiddleware { /// - 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, + configuration: DocCConfiguration, fileProvider: FileSystemProvider, logger: Logger, ) { self.logger = logger - self.configuration = configuration self.fileProvider = fileProvider + self.checkURI = .init(uriRoot: configuration.uriRoot) self.prepareURIPath = .init(uriRoot: configuration.uriRoot) self.redirectURI = .init(logger: logger) self.serveURI = .init( @@ -109,95 +108,98 @@ public struct DocCMiddleware { ) } + // MARK: Computed + + /// A list of relative root URI paths to match against the relative path of a resource. + var rootPaths: [String] {[ + .empty, .Path.forwardSlash + ]} + } // MARK: - RouterMiddleware extension DocCMiddleware: RouterMiddleware { - - // MARK: Type aliases - - public typealias Context = RequestContext - public typealias Input = Request - public typealias Output = Response - + // MARK: Functions public func handle( - _ request: Input, - context: any Context, - next: (Input, any Context) async throws -> Output + _ request: Request, + context: Context, + next: (Request, Context) async throws -> Response ) async throws -> Output { guard let uriPath = checkURI(request.uri), - let uriData = prepareURIPath(uriPath) + let resource = prepareURIPath(uriPath) else { return try await next(request, context) } - let rootPaths: [String] = [ - String(format: .Format.Path.root, uriData.archiveName), - String(format: .Format.Path.folder, uriData.archiveName) - ] + // Root URI Paths matching. + if rootPaths.contains(resource.relativePath) { + let uriRoot: String = if resource.relativePath.isEmpty { + .init(format: .Format.Path.forwardSlash, uriPath) + } else { + uriPath + } - if rootPaths.contains(uriData.resourcePath) { + // Rule #1: Redirects the URI path / or // to the path //documentation return redirectURI( - uriPath.hasSuffix(.Path.forwardSlash) - // Rule #2: Redirects the URI path // to the path //documentation - ? String(format: .Format.Path.documentation, uriPath) - // Rule #1: Redirects the URI path / to the path // - : String(format: .Format.Path.forwardSlash, uriPath), + String(format: .Format.Path.documentation, uriRoot), with: (request, context) ) } + // Asset files matching. for assetFile in AssetFile.allCases { - if uriData.resourcePath.contains(assetFile.path) { + if resource.relativePath.hasPrefix(assetFile.path) { return try await serveURI( assetFile == .documentation - // Rule #7: Redirects the URI path //data/documentation.json to the resource on /.doccarchive/data/documentation/.json - ? String(format: .Format.Path.documentationJSON, uriData.archiveReference) - // Rule #8: Redirects the URI path `//favicon.ico` to the resource on `/.doccarchive/favicon.ico` - // Rule #9: Redirects the URI path `//favicon.svg` to the resource on `/.doccarchive/favicon.svg` - // Rule #10: Redirects the URI path `//theme-settings.json` to the resource on `/.doccarchive/theme-settings.json` - : uriData.resourcePath, - at: uriData.archivePath, + // Rule #6: Redirects the URI path //data/documentation.json to the resource on /.doccarchive/data/documentation/.json + ? String(format: .Format.Path.documentationJSON, resource.archiveReference) + // Rule #7: Redirects the URI path `//favicon.ico` to the resource on `/.doccarchive/favicon.ico` + // Rule #8: Redirects the URI path `//favicon.svg` to the resource on `/.doccarchive/favicon.svg` + // Rule #9: Redirects the URI path `//theme-settings.json` to the resource on `/.doccarchive/theme-settings.json` + : resource.relativePath, + at: resource.archivePath, with: (request, context) ) } } for assetFolder in AssetFolder.allCases { - if uriData.resourcePath.contains(assetFolder.path) { - // Rule #11: Redirects the URI path `//css/` to the resource on `/.doccarchive/css/` - // Rule #12: Redirects the URI path `//data/` to the resource on `/.doccarchive/data/` - // Rule #13: Redirects the URI path `//downloads/` to the resource on `/.doccarchive/downloads/` - // Rule #14: Redirects the URI path `//images/` to the resource on `/.doccarchive/images/` - // Rule #15: Redirects the URI path `//img/` to the resource on `/.doccarchive/img/` - // Rule #16: Redirects the URI path `//index/` to the resource on `/.doccarchive/index/` - // Rule #17: Redirects the URI path `//js/` to the resource on `/.doccarchive/js/` - // Rule #18: Redirects the URI path `//videos/` to the resource on `/.doccarchive/videos/` + if resource.relativePath.hasPrefix(assetFolder.path) { + // Rule #10: Redirects the URI path `//css/` to the resource on `/.doccarchive/css/` + // Rule #11: Redirects the URI path `//data/` to the resource on `/.doccarchive/data/` + // Rule #12: Redirects the URI path `//downloads/` to the resource on `/.doccarchive/downloads/` + // Rule #13: Redirects the URI path `//images/` to the resource on `/.doccarchive/images/` + // Rule #14: Redirects the URI path `//img/` to the resource on `/.doccarchive/img/` + // Rule #15: Redirects the URI path `//index/` to the resource on `/.doccarchive/index/` + // Rule #16: Redirects the URI path `//js/` to the resource on `/.doccarchive/js/` + // Rule #17: Redirects the URI path `//videos/` to the resource on `/.doccarchive/videos/` return try await serveURI( - uriData.resourcePath, - at: uriData.archivePath, + resource.relativePath, + at: resource.archivePath, with: (request, context) ) } } - + for documentationFolder in DocumentationFolder.allCases { - if uriData.resourcePath.contains(documentationFolder.path) { - if uriData.resourcePath.hasSuffix(.Path.forwardSlash) { - // Rule #5: Redirects the URI path //documentation/ to the resource on /.doccarchive/documentation//index.html - // Rule #6: Redirects the URI path //tutorials/ to the resource on /.doccarchive/tutorials//index.html + if resource.relativePath.hasPrefix(documentationFolder.path) { + let pathSuffix: String = .init(format: .Format.Path.forwardSlash, documentationFolder.path) + + if uriPath.hasSuffix(pathSuffix) { + // Rule #4: Redirects the URI path //documentation/ to the resource on /.doccarchive/documentation//index.html + // Rule #5: Redirects the URI path //tutorials/ to the resource on /.doccarchive/tutorials//index.html return try await serveURI( - String(format: .Format.Path.index, documentationFolder.path, uriData.archiveReference), - at: uriData.archivePath, + String(format: .Format.Path.index, documentationFolder.path, resource.archiveReference), + at: resource.archivePath, with: (request, context) ) } else { - // Rule #3: Redirects the URI path //documentation to the path //documentation/ - // Rule #4: Redirects the URI path //tutorials to the path //tutorials/ + // Rule #2: Redirects the URI path //documentation to the path //documentation/ + // Rule #3: Redirects the URI path //tutorials to the path //tutorials/ return redirectURI( String(format: .Format.Path.forwardSlash, uriPath), with: (request, context) diff --git a/Tests/DocCMiddleware/Tests/Internal/Use Cases/CheckURIUseCaseTests.swift b/Tests/DocCMiddleware/Tests/Internal/Use Cases/CheckURIUseCaseTests.swift deleted file mode 100644 index 922fec2..0000000 --- a/Tests/DocCMiddleware/Tests/Internal/Use Cases/CheckURIUseCaseTests.swift +++ /dev/null @@ -1,116 +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 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 check_nonEncodedURIs( - uri uriPath: String, - expects result: String? - ) { - assertURI(uriPath, expects: result) - } - - @Test("check percent-encoded URIs", arguments: zip( - Input.percentEncodedURIs, - Output.percentEncodedURIs - )) - func check_percentEncodedURIs( - uri uriPath: String, - expects result: String? - ) { - assertURI(uriPath, expects: result) - } -#endif - -} - -// MARK: - Assertions - -private extension CheckURIUseCaseTests { - - // MARK: Functions - - /// 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. - func assertURI( - _ uriPath: String, - expects result: String? - ) { - // GIVEN - let uri = URI(uriPath) - - // WHEN - let output = useCase(uri) - - // THEN - #expect(output == result) - } - -} - -// MARK: - Constants - -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"] -} - -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. - static let percentEncodedURIs: [String?] = ["/", "/some/known?path", nil, nil, nil] -} diff --git a/Tests/DocCMiddleware/Tests/Internal/Enumerations/AssetFileTests.swift b/Tests/HummingbirdDocC/Tests/Internal/Enumerations/AssetFileTests.swift similarity index 97% rename from Tests/DocCMiddleware/Tests/Internal/Enumerations/AssetFileTests.swift rename to Tests/HummingbirdDocC/Tests/Internal/Enumerations/AssetFileTests.swift index 0ded28e..cd549d1 100644 --- a/Tests/DocCMiddleware/Tests/Internal/Enumerations/AssetFileTests.swift +++ b/Tests/HummingbirdDocC/Tests/Internal/Enumerations/AssetFileTests.swift @@ -12,7 +12,7 @@ import Testing -@testable import enum DocCMiddleware.AssetFile +@testable import enum HummingbirdDocC.AssetFile @Suite("Asset File", .tags(.enumeration)) struct AssetFileTests { diff --git a/Tests/DocCMiddleware/Tests/Internal/Enumerations/AssetFolderTests.swift b/Tests/HummingbirdDocC/Tests/Internal/Enumerations/AssetFolderTests.swift similarity index 97% rename from Tests/DocCMiddleware/Tests/Internal/Enumerations/AssetFolderTests.swift rename to Tests/HummingbirdDocC/Tests/Internal/Enumerations/AssetFolderTests.swift index a0b12d1..4af5efe 100644 --- a/Tests/DocCMiddleware/Tests/Internal/Enumerations/AssetFolderTests.swift +++ b/Tests/HummingbirdDocC/Tests/Internal/Enumerations/AssetFolderTests.swift @@ -12,7 +12,7 @@ import Testing -@testable import enum DocCMiddleware.AssetFolder +@testable import enum HummingbirdDocC.AssetFolder @Suite("Asset Folder", .tags(.enumeration)) struct AssetFolderTests { diff --git a/Tests/DocCMiddleware/Tests/Internal/Enumerations/DocumentationFolderTests.swift b/Tests/HummingbirdDocC/Tests/Internal/Enumerations/DocumentationFolderTests.swift similarity index 97% rename from Tests/DocCMiddleware/Tests/Internal/Enumerations/DocumentationFolderTests.swift rename to Tests/HummingbirdDocC/Tests/Internal/Enumerations/DocumentationFolderTests.swift index 3004b0c..f934d1a 100644 --- a/Tests/DocCMiddleware/Tests/Internal/Enumerations/DocumentationFolderTests.swift +++ b/Tests/HummingbirdDocC/Tests/Internal/Enumerations/DocumentationFolderTests.swift @@ -12,7 +12,7 @@ import Testing -@testable import enum DocCMiddleware.DocumentationFolder +@testable import enum HummingbirdDocC.DocumentationFolder @Suite("Documentation Type", .tags(.enumeration)) struct DocumentationTypeTests { diff --git a/Tests/DocCMiddleware/Tests/Internal/Extensions/LoggerMetadata+HelpersTests.swift b/Tests/HummingbirdDocC/Tests/Internal/Extensions/LoggerMetadata+HelpersTests.swift similarity index 99% rename from Tests/DocCMiddleware/Tests/Internal/Extensions/LoggerMetadata+HelpersTests.swift rename to Tests/HummingbirdDocC/Tests/Internal/Extensions/LoggerMetadata+HelpersTests.swift index 282552e..cbfe537 100644 --- a/Tests/DocCMiddleware/Tests/Internal/Extensions/LoggerMetadata+HelpersTests.swift +++ b/Tests/HummingbirdDocC/Tests/Internal/Extensions/LoggerMetadata+HelpersTests.swift @@ -17,7 +17,7 @@ import struct Hummingbird.HTTPResponse import struct Hummingbird.Request import struct Logging.Logger -@testable import DocCMiddleware +@testable import HummingbirdDocC @Suite("Logger Metadata Helpers", .tags(.extension)) struct LoggerMetadata_HelpersTests { diff --git a/Tests/HummingbirdDocC/Tests/Internal/Extensions/String_HelpersTests.swift b/Tests/HummingbirdDocC/Tests/Internal/Extensions/String_HelpersTests.swift new file mode 100644 index 0000000..f9d28db --- /dev/null +++ b/Tests/HummingbirdDocC/Tests/Internal/Extensions/String_HelpersTests.swift @@ -0,0 +1,97 @@ +// ===----------------------------------------------------------------------=== +// +// 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 HummingbirdDocC + +@Suite("String Helpers", .tags(.extension)) +struct String_HelpersTests { + + // MARK: Functions tests + +#if swift(>=6.2) + @Test(arguments: zip( + Input.prefixesToSubtract, + Output.prefixesToSubtract + )) + func `subtract`( + prefix: String, + expects newString: String? + ) { + assertSubtract( + string: .sample, + prefix: prefix, + expects: newString + ) + } +#else + @Test("subtract", arguments: zip( + Input.prefixesToSubtract, + Output.prefixesToSubtract + )) + func subtract( + prefix: String, + expects newString: String? + ) { + assertSubtract( + string: .sample, + prefix: prefix, + expects: newString + ) + } +#endif + +} + +// MARK: - Assertions + +private extension String_HelpersTests { + + // MARK: Functions + + /// Asserts a string subtraction. + /// - Parameters: + /// - string: A string from where the subtraction will occur. + /// - prefix: A prefix to subtract from a string. + /// - newString: An expected new string created out of the subtraction, if any. + func assertSubtract( + string: String, + prefix: String, + expects newString: String? + ) { + // GIVEN + // WHEN + let result = string.subtract(prefix) + + // THEN + #expect(result == newString) + } + +} + +// MARK: - Constants + +private extension Input { + /// A list of prefix strings. + static let prefixesToSubtract: [String] = ["Some", .sample, "some", "Else"] +} + +private extension Output { + /// A list of outcomes that are expected from subtracting the prefix substrings out of the sample string. + static let prefixesToSubtract: [String?] = ["thing", .empty, nil, nil] +} + +private extension String { + /// A sample string. + static let sample = "Something" +} diff --git a/Tests/HummingbirdDocC/Tests/Internal/Models/ResourceTests.swift b/Tests/HummingbirdDocC/Tests/Internal/Models/ResourceTests.swift new file mode 100644 index 0000000..a0da9e1 --- /dev/null +++ b/Tests/HummingbirdDocC/Tests/Internal/Models/ResourceTests.swift @@ -0,0 +1,106 @@ +// ===----------------------------------------------------------------------=== +// +// 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 HummingbirdDocC.Resource + +@Suite("Resource", .tags(.model)) +struct ResourceTests { + + // MARK: Properties tests + +#if swift(>=6.2) + @Test + func `archive path`() { + assertArchivePath( + archiveName: "SomeDocument", + expects: "/SomeDocument.doccarchive" + ) + } + + @Test + func `archive reference`() { + assertArchiveReference( + archiveName: "SomeDocument", + expects: "somedocument" + ) + } +#else + @Test("archive path") + func archivePath() { + assertArchivePath( + archiveName: "SomeDocument", + expects: "/SomeDocument.doccarchive" + ) + } + + @Test("archive reference") + func archiveReference() { + assertArchiveReference( + archiveName: "SomeDocument", + expects: "somedocument" + ) + } +#endif + +} + +// MARK: - Assertions + +private extension ResourceTests { + + // MARK: Functions + + /// Asserts the `archivePath` computed property of a resource. + /// - Parameters: + /// - archiveName: A name of the archive the resource belongs to. + /// - archivePath: An expected path to a documentation archive related to a given archive name. + func assertArchivePath( + archiveName: String, + expects archivePath: String + ) { + // GIVEN + var resource = Resource( + archiveName: archiveName, + relativePath: .empty + ) + + // WHEN + let result = resource.archivePath + + // THEN + #expect(result == archivePath) + } + + /// Asserts the `archiveReference` computed property of a resource. + /// - Parameters: + /// - archiveName: A name of the archive the resource belongs to. + /// - archiveReference: An expected reference related to a given archive name. + func assertArchiveReference( + archiveName: String, + expects archiveReference: String + ) { + // GIVEN + var resource = Resource( + archiveName: archiveName, + relativePath: .empty + ) + + // WHEN + let result = resource.archiveReference + + // THEN + #expect(result == archiveReference) + } + +} diff --git a/Tests/HummingbirdDocC/Tests/Internal/Use Cases/CheckURIUseCaseTests.swift b/Tests/HummingbirdDocC/Tests/Internal/Use Cases/CheckURIUseCaseTests.swift new file mode 100644 index 0000000..8efd8b2 --- /dev/null +++ b/Tests/HummingbirdDocC/Tests/Internal/Use Cases/CheckURIUseCaseTests.swift @@ -0,0 +1,147 @@ +// ===----------------------------------------------------------------------=== +// +// 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 HummingbirdDocC.CheckURIUseCase + +@Suite("Check URI use case", .tags(.useCase)) +struct CheckURIUseCaseTests { + + // 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 check_nonEncodedURIs( + uri uriPath: String, + expects result: String? + ) { + assertURI(uriPath, expects: result) + } + + @Test( + "check percent-encoded URIs", + arguments: zip( + Input.percentEncodedURIs, + Output.percentEncodedURIs + ) + ) + func check_percentEncodedURIs( + uri uriPath: String, + expects result: String? + ) { + assertURI(uriPath, expects: result) + } + #endif + +} + +// MARK: - Assertions + +extension CheckURIUseCaseTests { + + // MARK: Functions + + /// 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. + /// - uriRoot: A URI path that prefixes the `DocC` documentation resources. + /// - result: An expected result coming out of the use case. + fileprivate func assertURI( + _ uriPath: String, + uriRoot: String = .Sample.uriRoot, + expects result: String? + ) { + // GIVEN + let useCase = CheckURIUseCase(uriRoot: uriRoot) + let uri = URI(uriPath) + + // WHEN + let output = useCase(uri) + + // THEN + #expect(output == result) + } + +} + +// MARK: - Constants + +extension Input { + /// A list of non-encoded URI samples. + fileprivate static let nonEncodedURIs: [String] = [ + .Sample.uriRoot + .empty, + .Sample.uriRoot + .Path.forwardSlash, + .Sample.uriRoot + "/some/known/path", + .Sample.uriRoot + "/some/../path", + "some/other/root/some/known/path", + ] + /// A list of percent-encoded URI samples. + fileprivate static let percentEncodedURIs: [String] = [ + .Sample.uriRoot + "%2F", + .Sample.uriRoot + "/some%2Fknown%3Fpath", + .Sample.uriRoot + "/some/%2E%2E/path", + "some/other%2Froot/some%2Fknown%3Fpath", + ] +} + +extension Output { + /// A list of expected outputs for the non-encoded URI samples. + fileprivate static let nonEncodedURIs: [String?] = [ + .Sample.uriRoot, + .Sample.uriRoot + .Path.forwardSlash, + .Sample.uriRoot + "/some/known/path", + nil, + nil, + ] + /// A list of expected outputs for the percent-encoded URI samples. + fileprivate static let percentEncodedURIs: [String?] = [ + .Sample.uriRoot + .Path.forwardSlash, + .Sample.uriRoot + "/some/known?path", + nil, + nil, + ] +} diff --git a/Tests/DocCMiddleware/Tests/Internal/Use Cases/PrepareURIPathUseCaseTests.swift b/Tests/HummingbirdDocC/Tests/Internal/Use Cases/PrepareURIPathUseCaseTests.swift similarity index 69% rename from Tests/DocCMiddleware/Tests/Internal/Use Cases/PrepareURIPathUseCaseTests.swift rename to Tests/HummingbirdDocC/Tests/Internal/Use Cases/PrepareURIPathUseCaseTests.swift index e73ffa6..53aafb2 100644 --- a/Tests/DocCMiddleware/Tests/Internal/Use Cases/PrepareURIPathUseCaseTests.swift +++ b/Tests/HummingbirdDocC/Tests/Internal/Use Cases/PrepareURIPathUseCaseTests.swift @@ -12,7 +12,9 @@ import Testing -@testable import struct DocCMiddleware.PrepareURIPathUseCase +import struct HummingbirdDocC.Resource + +@testable import struct HummingbirdDocC.PrepareURIPathUseCase @Suite("Prepare URI Path Use Case", .tags(.useCase)) struct PrepareURIPathUseCaseTests { @@ -26,7 +28,7 @@ struct PrepareURIPathUseCaseTests { )) func `extract data with URI root not suffixed with forward slash`( uri uriPath: String, - expects result: PrepareURIPathUseCase.PreparedURIPaths? + expects result: Resource? ) throws { try assertData( uriRoot: .uriRoot, @@ -41,7 +43,7 @@ struct PrepareURIPathUseCaseTests { )) func `extract data with URI root suffixed with forward slash`( uri uriPath: String, - expects result: PrepareURIPathUseCase.PreparedURIPaths? + expects result: Resource? ) throws { try assertData( uriRoot: .uriRootSlashed, @@ -56,7 +58,7 @@ struct PrepareURIPathUseCaseTests { )) func data_withURIRoot_notSuffixed_withForwardSlash( uri uriPath: String, - expects result: PrepareURIPathUseCase.PreparedURIPaths? + expects result: Resource? ) throws { try assertData( uriRoot: .uriRoot, @@ -71,7 +73,7 @@ struct PrepareURIPathUseCaseTests { )) func data_withURIRoot_suffixed_withForwardSlash( uri uriPath: String, - expects result: PrepareURIPathUseCase.PreparedURIPaths? + expects result: Resource? ) throws { try assertData( uriRoot: .uriRootSlashed, @@ -94,30 +96,20 @@ private extension PrepareURIPathUseCaseTests { /// - 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. + /// - resource: An expected resource coming out of the use case, if any. func assertData( uriRoot: String, uriPath: String, - expects result: PrepareURIPathUseCase.PreparedURIPaths? + expects resource: Resource? ) throws { // GIVEN let useCase = PrepareURIPathUseCase(uriRoot: uriRoot) // WHEN - let output = useCase(uriPath) + let result = 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) - } + #expect(result == resource) } } @@ -126,29 +118,29 @@ private extension PrepareURIPathUseCaseTests { 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] + static let prepareURIPaths: [String] = [.uriRoot, .uriOffset, .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] + static let prepareURIPathsSlashed: [String] = [.uriRootSlashed, .uriOffsetSlashed, .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", "/SomeArchive.doccarchive", "/SomeArchive/some/content/path"), - (.empty, .empty, .empty, .Path.forwardSlash), + static let prepareURIPaths: [Resource?] = [ + .init(archiveName: .empty, relativePath: .Path.forwardSlash), + .init(archiveName: "SomeArchive", relativePath: "/some/content/path"), nil ] } private extension String { /// A root URI path to initialize the use case with. - static let uriRoot: Self = "/some/path" + static let uriRoot = "/some/path" /// A root URI path suffixed with a forward slash to initialize the use case with. - static let uriRootSlashed: Self = "/some/path/" + static let uriRootSlashed = "/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" + static let uriOffset = .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" + static let uriOffsetSlashed = .uriRootSlashed + "SomeArchive/some/content/path" /// A URI path not related to any root URI path. - static let uriOther: Self = "/some/other/path" + static let uriOther = "/some/other/path" } diff --git a/Tests/DocCMiddleware/Tests/Internal/Use Cases/RedirectURIUseCaseTests.swift b/Tests/HummingbirdDocC/Tests/Internal/Use Cases/RedirectURIUseCaseTests.swift similarity index 97% rename from Tests/DocCMiddleware/Tests/Internal/Use Cases/RedirectURIUseCaseTests.swift rename to Tests/HummingbirdDocC/Tests/Internal/Use Cases/RedirectURIUseCaseTests.swift index 29953a9..46c24ad 100644 --- a/Tests/DocCMiddleware/Tests/Internal/Use Cases/RedirectURIUseCaseTests.swift +++ b/Tests/HummingbirdDocC/Tests/Internal/Use Cases/RedirectURIUseCaseTests.swift @@ -18,7 +18,7 @@ import struct Hummingbird.HTTPResponse import struct Hummingbird.Request import struct Logging.Logger -@testable import struct DocCMiddleware.RedirectURIUseCase +@testable import struct HummingbirdDocC.RedirectURIUseCase @Suite("Redirect URI Use Case", .tags(.useCase)) struct RedirectURIUseCaseTests { @@ -107,7 +107,7 @@ private extension RedirectURIUseCaseTests { .contentLength: "0" ]) - let events = await logHandler.entries + let events = logHandler.entries if shouldEventBeLogged(logLevel) { #expect(!events.isEmpty) diff --git a/Tests/DocCMiddleware/Tests/Internal/Use Cases/ServeURIUseCaseTests.swift b/Tests/HummingbirdDocC/Tests/Internal/Use Cases/ServeURIUseCaseTests.swift similarity index 98% rename from Tests/DocCMiddleware/Tests/Internal/Use Cases/ServeURIUseCaseTests.swift rename to Tests/HummingbirdDocC/Tests/Internal/Use Cases/ServeURIUseCaseTests.swift index bf5d27e..5e18b20 100644 --- a/Tests/DocCMiddleware/Tests/Internal/Use Cases/ServeURIUseCaseTests.swift +++ b/Tests/HummingbirdDocC/Tests/Internal/Use Cases/ServeURIUseCaseTests.swift @@ -18,7 +18,7 @@ import struct Hummingbird.HTTPResponse import struct Hummingbird.Request import struct Logging.Logger -@testable import struct DocCMiddleware.ServeURIUseCase +@testable import struct HummingbirdDocC.ServeURIUseCase @Suite("Serve URI Use Case", .tags(.useCase)) struct ServeURIUseCaseTests { @@ -190,7 +190,7 @@ private extension ServeURIUseCaseTests { #expect(contentLength == 0) } - let events = await logHandler.entries + let events = logHandler.entries if shouldEventBeLogged( logLevel: logLevel, diff --git a/Tests/DocCMiddleware/Tests/Public/Middlewares/DocCMiddlewareTests.swift b/Tests/HummingbirdDocC/Tests/Public/Middlewares/DocCMiddlewareTests.swift similarity index 90% rename from Tests/DocCMiddleware/Tests/Public/Middlewares/DocCMiddlewareTests.swift rename to Tests/HummingbirdDocC/Tests/Public/Middlewares/DocCMiddlewareTests.swift index 3eec0dd..d73c2d5 100644 --- a/Tests/DocCMiddleware/Tests/Public/Middlewares/DocCMiddlewareTests.swift +++ b/Tests/HummingbirdDocC/Tests/Public/Middlewares/DocCMiddlewareTests.swift @@ -20,7 +20,8 @@ import struct Hummingbird.LocalFileSystem import struct Hummingbird.Request import struct Logging.Logger -@testable import struct DocCMiddleware.DocCMiddleware +@testable import struct HummingbirdDocC.DocCConfiguration +@testable import struct HummingbirdDocC.DocCMiddleware @Suite("DocC Middleware", .tags(.middleware)) struct DocCMiddlewareTests { @@ -290,21 +291,17 @@ private extension DocCMiddlewareTests { /// - configuration: A type that contains the parameters to configure the middleware. /// - logger: A type that interacts with the logging system. func assertInit( - configuration: DocCMiddleware.Configuration, + configuration: DocCConfiguration, logger: Logger = .test() ) { // GIVEN // WHEN - let middleware = DocCMiddleware( + let middleware = DocCMiddleware( configuration: configuration, logger: logger ) // THEN - #expect(middleware.configuration.folderRoot == configuration.folderRoot) - #expect(middleware.configuration.uriRoot == configuration.uriRoot) - #expect(middleware.configuration.threadPool === configuration.threadPool) - #expect(middleware.logger.label == logger.label) #expect(middleware.logger.logLevel == logger.logLevel) #expect(middleware.logger.metadataProvider == nil) @@ -318,23 +315,19 @@ private extension DocCMiddlewareTests { /// - 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, + configuration: DocCConfiguration, logger: Logger = .test(), fileProvider: FileSystemProvider ) { // GIVEN // WHEN - let middleware = DocCMiddleware( + let middleware = DocCMiddleware( configuration: configuration, fileProvider: fileProvider, logger: logger ) // THEN - #expect(middleware.configuration.folderRoot == configuration.folderRoot) - #expect(middleware.configuration.uriRoot == configuration.uriRoot) - #expect(middleware.configuration.threadPool === configuration.threadPool) - #expect(middleware.logger.label == logger.label) #expect(middleware.logger.logLevel == logger.logLevel) #expect(middleware.logger.metadataProvider == nil) @@ -362,13 +355,13 @@ private extension DocCMiddlewareTests { handler: logHandler ) - let context: any RequestContext = RequestContextMock(logger: logger) + let context: RequestContextMock = .init(logger: logger) let request: Request = .test( method: .get, path: uriPath ) - let middleware = DocCMiddleware( + let middleware = DocCMiddleware( configuration: .init( uriRoot: .Sample.uriRoot, folderRoot: .Sample.uriFolder @@ -385,7 +378,7 @@ private extension DocCMiddlewareTests { // THEN #expect(result.status == statusCode) - let events = await logHandler.entries + let events = logHandler.entries if statusCode == .movedPermanently, let uriRedirect { #expect(result.body.contentLength == 0) @@ -448,13 +441,13 @@ private extension DocCMiddlewareTests { default: .init(fileIdentifier: .init(), shouldLoadFile: false) } - let context: any RequestContext = RequestContextMock(logger: logger) + let context: RequestContextMock = .init(logger: logger) let request: Request = .test( method: .get, path: uriPath ) - let middleware = DocCMiddleware( + let middleware = DocCMiddleware( configuration: .init( uriRoot: .Sample.uriRoot, folderRoot: .Sample.uriFolder @@ -483,7 +476,7 @@ private extension DocCMiddlewareTests { #expect(contentLength == 0) } - let events = await logHandler.entries + let events = logHandler.entries if shouldEventBeLogged( logLevel: logLevel, @@ -552,7 +545,12 @@ private extension DocCMiddlewareTests { private extension Input { /// A list of relative URI paths to match against the URI path redirections done by the middleware. - static let redirectURIPaths: [String] = [.empty, .Path.forwardSlash, "/documentation", "/tutorials"] + static let redirectURIPaths: [String] = [ + .empty, + .Path.forwardSlash, + "/documentation", + "/tutorials" + ] /// A list of relative URI paths to match against the URI path servings done by the middleware. static let serveURIPaths: [String] = [ "/documentation/", @@ -574,22 +572,27 @@ private extension Input { private extension Output { /// A list of expected relative URI path redirections outputs coming out of the URI path redirections done by the middleware. - static let redirectURIPaths: [String] = [.Path.forwardSlash, "/documentation", "/documentation/", "/tutorials/"] + static let redirectURIPaths: [String] = [ + "/documentation", + "/documentation", + "/documentation/", + "/tutorials/" + ] /// A list of expected relative file URI paths of the logged messages coming out of the URI path servings done by the middleware. static let serveURIFilePaths: [String] = [ "/SomeDocument.doccarchive/documentation/somedocument/index.html", "/SomeDocument.doccarchive/tutorials/somedocument/index.html", "/SomeDocument.doccarchive/data/documentation/somedocument.json", - "/SomeDocument.doccarchive/SomeDocument/favicon.ico", - "/SomeDocument.doccarchive/SomeDocument/favicon.svg", - "/SomeDocument.doccarchive/SomeDocument/theme-settings.json", - "/SomeDocument.doccarchive/SomeDocument/css/file.css", - "/SomeDocument.doccarchive/SomeDocument/data/data.bin", - "/SomeDocument.doccarchive/SomeDocument/downloads/file.txt", - "/SomeDocument.doccarchive/SomeDocument/images/image.png", - "/SomeDocument.doccarchive/SomeDocument/img/image.jpg", - "/SomeDocument.doccarchive/SomeDocument/index/file", - "/SomeDocument.doccarchive/SomeDocument/js/file.js", - "/SomeDocument.doccarchive/SomeDocument/videos/video.mp4" + "/SomeDocument.doccarchive/favicon.ico", + "/SomeDocument.doccarchive/favicon.svg", + "/SomeDocument.doccarchive/theme-settings.json", + "/SomeDocument.doccarchive/css/file.css", + "/SomeDocument.doccarchive/data/data.bin", + "/SomeDocument.doccarchive/downloads/file.txt", + "/SomeDocument.doccarchive/images/image.png", + "/SomeDocument.doccarchive/img/image.jpg", + "/SomeDocument.doccarchive/index/file", + "/SomeDocument.doccarchive/js/file.js", + "/SomeDocument.doccarchive/videos/video.mp4" ] } diff --git a/Tests/DocCMiddleware/Types/Extensions/Logger+Helpers.swift b/Tests/HummingbirdDocC/Types/Extensions/Logger+Helpers.swift similarity index 95% rename from Tests/DocCMiddleware/Types/Extensions/Logger+Helpers.swift rename to Tests/HummingbirdDocC/Types/Extensions/Logger+Helpers.swift index d0cec00..70e6b5f 100644 --- a/Tests/DocCMiddleware/Types/Extensions/Logger+Helpers.swift +++ b/Tests/HummingbirdDocC/Types/Extensions/Logger+Helpers.swift @@ -53,5 +53,5 @@ extension Logger { private extension String { /// A label to assign to a test logger instance. - static let loggerLabel = "test.hummingbird-docc-middleware.logger" + static let loggerLabel = "test.hummingbird-docc.logger" } diff --git a/Tests/DocCMiddleware/Types/Extensions/LoggerLevel+Helpers.swift b/Tests/HummingbirdDocC/Types/Extensions/LoggerLevel+Helpers.swift similarity index 100% rename from Tests/DocCMiddleware/Types/Extensions/LoggerLevel+Helpers.swift rename to Tests/HummingbirdDocC/Types/Extensions/LoggerLevel+Helpers.swift diff --git a/Tests/DocCMiddleware/Types/Extensions/Request+Helpers.swift b/Tests/HummingbirdDocC/Types/Extensions/Request+Helpers.swift similarity index 100% rename from Tests/DocCMiddleware/Types/Extensions/Request+Helpers.swift rename to Tests/HummingbirdDocC/Types/Extensions/Request+Helpers.swift diff --git a/Tests/DocCMiddleware/Types/Extensions/String+Constants.swift b/Tests/HummingbirdDocC/Types/Extensions/String+Constants.swift similarity index 100% rename from Tests/DocCMiddleware/Types/Extensions/String+Constants.swift rename to Tests/HummingbirdDocC/Types/Extensions/String+Constants.swift diff --git a/Tests/DocCMiddleware/Types/Extensions/Tag+Constants.swift b/Tests/HummingbirdDocC/Types/Extensions/Tag+Constants.swift similarity index 91% rename from Tests/DocCMiddleware/Types/Extensions/Tag+Constants.swift rename to Tests/HummingbirdDocC/Types/Extensions/Tag+Constants.swift index 31b3733..3ec4dc1 100644 --- a/Tests/DocCMiddleware/Types/Extensions/Tag+Constants.swift +++ b/Tests/HummingbirdDocC/Types/Extensions/Tag+Constants.swift @@ -22,6 +22,8 @@ extension Tag { @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 model type. + @Tag static var model: Self /// Tag that indicate a test case for a use case type. @Tag static var useCase: Self diff --git a/Tests/DocCMiddleware/Types/Mocks/FileProviderMock.swift b/Tests/HummingbirdDocC/Types/Mocks/FileProviderMock.swift similarity index 100% rename from Tests/DocCMiddleware/Types/Mocks/FileProviderMock.swift rename to Tests/HummingbirdDocC/Types/Mocks/FileProviderMock.swift diff --git a/Tests/DocCMiddleware/Types/Mocks/LogHandlerMock.swift b/Tests/HummingbirdDocC/Types/Mocks/LogHandlerMock.swift similarity index 75% rename from Tests/DocCMiddleware/Types/Mocks/LogHandlerMock.swift rename to Tests/HummingbirdDocC/Types/Mocks/LogHandlerMock.swift index b5c35ce..fa67e3c 100644 --- a/Tests/DocCMiddleware/Types/Mocks/LogHandlerMock.swift +++ b/Tests/HummingbirdDocC/Types/Mocks/LogHandlerMock.swift @@ -33,9 +33,7 @@ struct LogHandlerMock { // MARK: Computed /// A list of all the logged events that are being persisted in the recorder. - var entries: [LogEntry] { - get async { await recorder.entries } - } + var entries: [LogEntry] { recorder.entries } } @@ -63,13 +61,25 @@ struct LogEntry: Equatable { // MARK: - LogRecorder extension LogHandlerMock { - /// An actor that persists all the events logged by the ``LogHandlerMock`` mock handler. - actor LogRecorder { + /// A class that records all the events logged by the ``LogHandlerMock`` mock handler. + /// + /// This class conforms to the `Sendable` protocol by using the `@unchecked` modifier because a `NSLock`type is used to handle the access to the logged events in a thread-safe way. + final class LogRecorder: @unchecked Sendable { // MARK: Properties + /// A list of all the logged events persisted in a thread-safe way. + private(set) var _entries: [LogEntry] = [] + + /// A type that coordinates the access to the persisted logged events in a thread-safe way. + private let lock: NSLock = .init() + + // MARK: Computed + /// A list of all the logged events. - private(set) var entries: [LogEntry] = [] + var entries: [LogEntry] { + lock.withLock { _entries } + } // MARK: Functions @@ -84,13 +94,15 @@ extension LogHandlerMock { metadata: Logger.Metadata?, message: Logger.Message, source: String - ) async { - entries += [.init( - level: level, - metadata: metadata, - message: message, - source: source - )] + ) { + lock.withLock { + _entries += [.init( + level: level, + metadata: metadata, + message: message, + source: source + )] + } } } @@ -130,12 +142,12 @@ extension LogHandlerMock: LogHandler { function: String, line: UInt ) { - Task { await recorder.record( + recorder.record( level: level, metadata: metadata, message: message, source: source - )} + ) } } diff --git a/Tests/DocCMiddleware/Types/Mocks/RequestContextMock.swift b/Tests/HummingbirdDocC/Types/Mocks/RequestContextMock.swift similarity index 100% rename from Tests/DocCMiddleware/Types/Mocks/RequestContextMock.swift rename to Tests/HummingbirdDocC/Types/Mocks/RequestContextMock.swift diff --git a/Tests/DocCMiddleware/Types/Namespaces/Input.swift b/Tests/HummingbirdDocC/Types/Namespaces/Input.swift similarity index 100% rename from Tests/DocCMiddleware/Types/Namespaces/Input.swift rename to Tests/HummingbirdDocC/Types/Namespaces/Input.swift diff --git a/Tests/DocCMiddleware/Types/Namespaces/Output.swift b/Tests/HummingbirdDocC/Types/Namespaces/Output.swift similarity index 100% rename from Tests/DocCMiddleware/Types/Namespaces/Output.swift rename to Tests/HummingbirdDocC/Types/Namespaces/Output.swift diff --git a/Tests/DocCMiddleware/Types/Stubs/FileProviderStub.swift b/Tests/HummingbirdDocC/Types/Stubs/FileProviderStub.swift similarity index 100% rename from Tests/DocCMiddleware/Types/Stubs/FileProviderStub.swift rename to Tests/HummingbirdDocC/Types/Stubs/FileProviderStub.swift