Implemented the DocC archives support for the middleware (#2)
This PR contains the work done to implement the support for `DocC` archives (or `.doccarchive` containers) into the middleware. Reviewed-on: rock-n-code/hummingbird-docc-middleware#2 Co-authored-by: Javier Cicchelli <javier@rock-n-code.com> Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
This commit was merged in pull request #2.
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// 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 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 values.
|
||||
enum Path {
|
||||
/// A forwarding slash.
|
||||
static let forwardSlash = "/"
|
||||
/// An indication of a previous folder in a path component.
|
||||
static let previousFolder = ".."
|
||||
}
|
||||
}
|
||||
@@ -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 = "/%@"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 protocol that provides a relative path representation.
|
||||
protocol Pathable {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// A (relative) path to a resource.
|
||||
var path: String { get }
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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(.Path.previousFolder),
|
||||
uriPath.hasPrefix(.Path.forwardSlash)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return uriPath
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// 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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// 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.Response
|
||||
import struct Logging.Logger
|
||||
|
||||
/// A use case that produces a redirect response based on a given URI path.
|
||||
struct RedirectURIUseCase {
|
||||
|
||||
// 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: .Logging.source
|
||||
)
|
||||
}
|
||||
|
||||
return .redirect(
|
||||
to: uriPath,
|
||||
type: .permanent
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 ServeURIUseCase<Provider: FileProvider> {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// A type that conforms to a protocol that defines file system interactions.
|
||||
private let fileProvider: Provider
|
||||
|
||||
/// 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: Provider,
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// 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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the Hummingbird DocC Middleware open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
|
||||
// Licensed under the EUPL 1.2 or later.
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
import protocol Hummingbird.FileProvider
|
||||
import protocol Hummingbird.RequestContext
|
||||
import protocol Hummingbird.RouterMiddleware
|
||||
|
||||
import struct Hummingbird.LocalFileSystem
|
||||
import struct Hummingbird.Request
|
||||
import struct Hummingbird.Response
|
||||
import struct Logging.Logger
|
||||
|
||||
/// A middleware that proxies requests to `DocC` documentation containers within a hosting app.
|
||||
///
|
||||
/// This middleware routes the contents of a `DocC` documentation container, defined by its resource URI paths, following these rules:
|
||||
///
|
||||
/// 1. *Redirects the URI path `/<ArchiveName>` to the path `/<ArchiveName>/`*;
|
||||
/// 2. *Redirects the URI path `/<ArchiveName>/` to the path `/<ArchiveName>/documentation`*
|
||||
/// 3. *Redirects the URI path `/<ArchiveName>/documentation` to the path `/<ArchiveName>/documentation/`*
|
||||
/// 4. *Redirects the URI path `/<ArchiveName>/tutorials` to the path `/<ArchiveName>/tutorials/`*
|
||||
/// 5. *Redirects the URI path `/<ArchiveName>/documentation/` to the resource on `/<ArchiveName>.doccarchive/documentation/<ArchiveReference>/index.html`*
|
||||
/// 6. *Redirects the URI path `/<ArchiveName>/tutorials/` to the resource on `/<ArchiveName>.doccarchive/tutorials/<ArchiveReference>/index.html`*
|
||||
/// 7. *Redirects the URI path `/<ArchiveName>/data/documentation.json` to the resource on `/<ArchiveName>.doccarchive/data/documentation/<ArchiveReference>.json`*
|
||||
/// 8. *Redirects the URI path `/<ArchiveName>/favicon.ico` to the resource on `/<ArchiveName>.doccarchive/favicon.ico`*
|
||||
/// 9. *Redirects the URI path `/<ArchiveName>/favicon.svg` to the resource on `/<ArchiveName>.doccarchive/favicon.svg`*
|
||||
/// 10. *Redirects the URI path `/<ArchiveName>/theme-settings.json` to the resource on `/<ArchiveName>.doccarchive/theme-settings.json`*
|
||||
/// 11. *Redirects the URI path `/<ArchiveName>/css/<path/to/file>` to the resource on `/<ArchiveName>.doccarchive/css/<path/to/file>`*
|
||||
/// 12. *Redirects the URI path `/<ArchiveName>/data/<path/to/file>` to the resource on `/<ArchiveName>.doccarchive/data/<path/to/file>`*
|
||||
/// 13. *Redirects the URI path `/<ArchiveName>/downloads/<path/to/file>` to the resource on `/<ArchiveName>.doccarchive/downloads/<path/to/file>`*
|
||||
/// 14. *Redirects the URI path `/<ArchiveName>/images/<path/to/file>` to the resource on `/<ArchiveName>.doccarchive/images/<path/to/file>`*
|
||||
/// 15. *Redirects the URI path `/<ArchiveName>/img/<path/to/file>` to the resource on `/<ArchiveName>.doccarchive/img/<path/to/file>`*
|
||||
/// 16. *Redirects the URI path `/<ArchiveName>/index/<path/to/file>` to the resource on `/<ArchiveName>.doccarchive/index/<path/to/file>`*
|
||||
/// 17. *Redirects the URI path `/<ArchiveName>/js/<path/to/file>` to the resource on `/<ArchiveName>.doccarchive/js/<path/to/file>`*
|
||||
/// 18. *Redirects the URI path `/<ArchiveName>/videos/<path/to/file>` to the resource on `/<ArchiveName>.doccarchive/videos/<path/to/file>`*
|
||||
///
|
||||
public struct DocCMiddleware<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
|
||||
|
||||
/// A type that interacts with the logging system.
|
||||
let logger: Logger
|
||||
|
||||
/// A use case that checks whether a received URI could be processed or not.
|
||||
private let checkURI: CheckURIUseCase = .init()
|
||||
|
||||
/// A use case that extracts data from a given URI path, essential for routing the documentation contents.
|
||||
private let prepareURIPath: PrepareURIPathUseCase
|
||||
|
||||
/// A use case that produces a redirect response based on a given URI path.
|
||||
private let redirectURI: RedirectURIUseCase
|
||||
|
||||
/// A use case that serves a resource, defined by its URI path, from a physical location.
|
||||
private let serveURI: ServeURIUseCase<FileSystemProvider>
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Initializes this middleware.
|
||||
/// - Parameters:
|
||||
/// - configuration: A type that contains the parameters to configure the middleware.
|
||||
/// - logger: A type that interacts with the logging system.
|
||||
public init(
|
||||
configuration: Configuration,
|
||||
logger: Logger
|
||||
) where FileSystemProvider == LocalFileSystem {
|
||||
self.init(
|
||||
configuration: configuration,
|
||||
fileProvider: LocalFileSystem(
|
||||
rootFolder: configuration.folderRoot,
|
||||
threadPool: configuration.threadPool,
|
||||
logger: logger
|
||||
),
|
||||
logger: logger,
|
||||
)
|
||||
}
|
||||
|
||||
/// Initializes this middleware with a concrete file provider type.
|
||||
/// - Parameters:
|
||||
/// - configuration: A type that contains the parameters to configure the middleware.
|
||||
/// - fileProvider: A type that conforms to the protocol that defines file system interactions.
|
||||
/// - logger: A type that interacts with the logging system.
|
||||
init(
|
||||
configuration: Configuration,
|
||||
fileProvider: FileSystemProvider,
|
||||
logger: Logger,
|
||||
) {
|
||||
self.logger = logger
|
||||
self.configuration = configuration
|
||||
self.fileProvider = fileProvider
|
||||
self.prepareURIPath = .init(uriRoot: configuration.uriRoot)
|
||||
self.redirectURI = .init(logger: logger)
|
||||
self.serveURI = .init(
|
||||
fileProvider: fileProvider,
|
||||
logger: logger
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - RouterMiddleware
|
||||
|
||||
extension DocCMiddleware: RouterMiddleware {
|
||||
|
||||
// MARK: Type aliases
|
||||
|
||||
public typealias Context = RequestContext
|
||||
public typealias Input = Request
|
||||
public typealias Output = Response
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
public func handle(
|
||||
_ request: Input,
|
||||
context: any Context,
|
||||
next: (Input, any Context) async throws -> Output
|
||||
) async throws -> Output {
|
||||
guard
|
||||
let uriPath = checkURI(request.uri),
|
||||
let uriData = prepareURIPath(uriPath)
|
||||
else {
|
||||
return try await next(request, context)
|
||||
}
|
||||
|
||||
let rootPaths: [String] = [
|
||||
String(format: .Format.Path.root, uriData.archiveName),
|
||||
String(format: .Format.Path.folder, uriData.archiveName)
|
||||
]
|
||||
|
||||
if rootPaths.contains(uriData.resourcePath) {
|
||||
return redirectURI(
|
||||
uriPath.hasSuffix(.Path.forwardSlash)
|
||||
// Rule #2: Redirects the URI path /<ArchiveName>/ to the path /<ArchiveName>/documentation
|
||||
? String(format: .Format.Path.documentation, uriPath)
|
||||
// Rule #1: Redirects the URI path /<ArchiveName> to the path /<ArchiveName>/
|
||||
: String(format: .Format.Path.forwardSlash, uriPath),
|
||||
with: (request, context)
|
||||
)
|
||||
}
|
||||
|
||||
for assetFile in AssetFile.allCases {
|
||||
if uriData.resourcePath.contains(assetFile.path) {
|
||||
return try await serveURI(
|
||||
assetFile == .documentation
|
||||
// Rule #7: Redirects the URI path /<ArchiveName>/data/documentation.json to the resource on /<ArchiveName>.doccarchive/data/documentation/<ArchiveReference>.json
|
||||
? String(format: .Format.Path.documentationJSON, uriData.archiveReference)
|
||||
// Rule #8: Redirects the URI path `/<ArchiveName>/favicon.ico` to the resource on `/<ArchiveName>.doccarchive/favicon.ico`
|
||||
// Rule #9: Redirects the URI path `/<ArchiveName>/favicon.svg` to the resource on `/<ArchiveName>.doccarchive/favicon.svg`
|
||||
// Rule #10: Redirects the URI path `/<ArchiveName>/theme-settings.json` to the resource on `/<ArchiveName>.doccarchive/theme-settings.json`
|
||||
: uriData.resourcePath,
|
||||
at: uriData.archivePath,
|
||||
with: (request, context)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for assetFolder in AssetFolder.allCases {
|
||||
if uriData.resourcePath.contains(assetFolder.path) {
|
||||
// Rule #11: Redirects the URI path `/<ArchiveName>/css/<path/to/file>` to the resource on `/<ArchiveName>.doccarchive/css/<path/to/file>`
|
||||
// Rule #12: Redirects the URI path `/<ArchiveName>/data/<path/to/file>` to the resource on `/<ArchiveName>.doccarchive/data/<path/to/file>`
|
||||
// Rule #13: Redirects the URI path `/<ArchiveName>/downloads/<path/to/file>` to the resource on `/<ArchiveName>.doccarchive/downloads/<path/to/file>`
|
||||
// Rule #14: Redirects the URI path `/<ArchiveName>/images/<path/to/file>` to the resource on `/<ArchiveName>.doccarchive/images/<path/to/file>`
|
||||
// Rule #15: Redirects the URI path `/<ArchiveName>/img/<path/to/file>` to the resource on `/<ArchiveName>.doccarchive/img/<path/to/file>`
|
||||
// Rule #16: Redirects the URI path `/<ArchiveName>/index/<path/to/file>` to the resource on `/<ArchiveName>.doccarchive/index/<path/to/file>`
|
||||
// Rule #17: Redirects the URI path `/<ArchiveName>/js/<path/to/file>` to the resource on `/<ArchiveName>.doccarchive/js/<path/to/file>`
|
||||
// Rule #18: Redirects the URI path `/<ArchiveName>/videos/<path/to/file>` to the resource on `/<ArchiveName>.doccarchive/videos/<path/to/file>`
|
||||
return try await serveURI(
|
||||
uriData.resourcePath,
|
||||
at: uriData.archivePath,
|
||||
with: (request, context)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for documentationFolder in DocumentationFolder.allCases {
|
||||
if uriData.resourcePath.contains(documentationFolder.path) {
|
||||
if uriData.resourcePath.hasSuffix(.Path.forwardSlash) {
|
||||
// Rule #5: Redirects the URI path /<ArchiveName>/documentation/ to the resource on /<ArchiveName>.doccarchive/documentation/<ArchiveReference>/index.html
|
||||
// Rule #6: Redirects the URI path /<ArchiveName>/tutorials/ to the resource on /<ArchiveName>.doccarchive/tutorials/<ArchiveReference>/index.html
|
||||
return try await serveURI(
|
||||
String(format: .Format.Path.index, documentationFolder.path, uriData.archiveReference),
|
||||
at: uriData.archivePath,
|
||||
with: (request, context)
|
||||
)
|
||||
} else {
|
||||
// Rule #3: Redirects the URI path /<ArchiveName>/documentation to the path /<ArchiveName>/documentation/
|
||||
// Rule #4: Redirects the URI path /<ArchiveName>/tutorials to the path /<ArchiveName>/tutorials/
|
||||
return redirectURI(
|
||||
String(format: .Format.Path.forwardSlash, uriPath),
|
||||
with: (request, context)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return try await next(request, context)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
// The Swift Programming Language
|
||||
// https://docs.swift.org/swift-book
|
||||
@@ -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
|
||||
|
||||
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"]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
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/"]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
private extension Output {
|
||||
/// A list of expected outputs for the paths of the ``DocumentationFolder`` enumeration cases.
|
||||
static let documentationFolderPaths: [String] = ["/documentation", "/tutorials"]
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// 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: .Sample.uriRedirection
|
||||
)
|
||||
}
|
||||
#else
|
||||
@Test("metadata with HTTP method and status code")
|
||||
func metadata_withMethod_andStatusCode() throws {
|
||||
assertMetadata(
|
||||
method: try randomMethod,
|
||||
statusCode: try randomStatusCode
|
||||
)
|
||||
}
|
||||
|
||||
@Test("metadata with HTTP method, status code and redirection URI path")
|
||||
func metadata_withMethod_statusCode_andRedirection() 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: RequestContextMock = .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())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 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]
|
||||
}
|
||||
@@ -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 data_withURIRoot_notSuffixed_withForwardSlash(
|
||||
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 data_withURIRoot_suffixed_withForwardSlash(
|
||||
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", "/SomeArchive.doccarchive", "/SomeArchive/some/content/path"),
|
||||
(.empty, .empty, .empty, .Path.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"
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// 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.RedirectURIUseCase
|
||||
|
||||
@Suite("Redirect URI Use Case", .tags(.useCase))
|
||||
struct RedirectURIUseCaseTests {
|
||||
|
||||
// MARK: Use case tests
|
||||
|
||||
#if swift(>=6.2)
|
||||
@Test
|
||||
func `response when logging event triggered`() async throws {
|
||||
try await assertResponse(
|
||||
logLevel: try .random(upTo: .debug),
|
||||
uriRedirection: .Sample.uriRedirection,
|
||||
expects: .movedPermanently
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `response when logging event not triggered`() async throws {
|
||||
try await assertResponse(
|
||||
logLevel: try .random(fromExclusive: .debug),
|
||||
uriRedirection: .Sample.uriRedirection,
|
||||
expects: .movedPermanently
|
||||
)
|
||||
}
|
||||
#else
|
||||
@Test("response when logging event triggered")
|
||||
func response_whenEventTriggered() async throws {
|
||||
try await assertResponse(
|
||||
logLevel: try .random(upTo: .debug),
|
||||
uriRedirection: .uriRedirection,
|
||||
expects: .movedPermanently
|
||||
)
|
||||
}
|
||||
|
||||
@Test("response when logging event not triggered")
|
||||
func response_whenEventNotTriggered() async throws {
|
||||
try await assertResponse(
|
||||
logLevel: try randomLogLevelWithNoEvent,
|
||||
uriRedirection: .uriRedirection,
|
||||
expects: .movedPermanently
|
||||
)
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Assertions
|
||||
|
||||
private extension RedirectURIUseCaseTests {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// 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.
|
||||
/// - 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: .Logging.source
|
||||
))
|
||||
} else {
|
||||
#expect(events.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension RedirectURIUseCaseTests {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// 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.debug, .trace].contains(logLevel)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// 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.ServeURIUseCase
|
||||
|
||||
@Suite("Serve URI Use Case", .tags(.useCase))
|
||||
struct ServeURIUseCaseTests {
|
||||
|
||||
// MARK: Use case tests
|
||||
|
||||
#if swift(>=6.2)
|
||||
@Test
|
||||
func `response when resource served and logging event triggered`() async throws {
|
||||
try await assertResponse(
|
||||
logLevel: try .random(upTo: .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: try .random(fromExclusive: .debug),
|
||||
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: try .random(upTo: .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: try .random(fromExclusive: .error),
|
||||
uriPath: .Sample.uriResource,
|
||||
folderPath: .Sample.uriFolder,
|
||||
expects: .notFound
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `response throws error when loading resource`() async throws {
|
||||
try await assertResponse(
|
||||
logLevel: try .random(),
|
||||
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: try .random(upTo: .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: try .random(fromExclusive: .debug),
|
||||
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: try .random(upTo: .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: try .random(fromExclusive: .error),
|
||||
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: try .random(),
|
||||
uriPath: .Sample.uriResource,
|
||||
folderPath: .Sample.uriFolder
|
||||
)
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Assertions
|
||||
|
||||
private extension ServeURIUseCaseTests {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// - 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 = ServeURIUseCase(
|
||||
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 ServeURIUseCaseTests {
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,595 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// 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.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
|
||||
|
||||
@Suite("DocC Middleware", .tags(.middleware))
|
||||
struct DocCMiddlewareTests {
|
||||
|
||||
// MARK: Initializers tests
|
||||
|
||||
#if swift(>=6.2)
|
||||
@Test
|
||||
func `initialize with URI and folder paths`() {
|
||||
assertInit(configuration: .init(
|
||||
uriRoot: .Sample.uriResource,
|
||||
folderRoot: .Sample.uriFolder
|
||||
))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `initialize with URI path and type that conforms to the FileProvider protocol`() {
|
||||
assertInit(
|
||||
configuration: .init(
|
||||
uriRoot: .Sample.uriResource,
|
||||
folderRoot: .empty
|
||||
),
|
||||
fileProvider: FileProviderStub()
|
||||
)
|
||||
}
|
||||
#else
|
||||
@Test("initialize with URI and folder paths")
|
||||
func init_withURI_andFolderPaths() {
|
||||
assertInit(configuration: .init(
|
||||
uriRoot: .Sample.uriResource,
|
||||
folderRoot: .Sample.uriFolder
|
||||
))
|
||||
}
|
||||
|
||||
@Test("initialize with type that conforms to the FileProvider protocol")
|
||||
func init_withURI_path_andFileProviderType() {
|
||||
assertInit(
|
||||
configuration: .init(
|
||||
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 .random(upTo: .debug),
|
||||
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 .random(fromExclusive: .debug),
|
||||
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 .random(),
|
||||
uriPath: 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 .random(upTo: .debug),
|
||||
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 .random(fromExclusive: .debug),
|
||||
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 .random(upTo: .error),
|
||||
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 .random(fromExclusive: .error),
|
||||
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 .random(),
|
||||
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 .random(upTo: .debug),
|
||||
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 .random(fromExclusive: .debug),
|
||||
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 .random(),
|
||||
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 .random(upTo: .debug),
|
||||
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 .random(fromExclusive: .debug),
|
||||
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 .random(upTo: .error),
|
||||
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 .random(fromExclusive: .error),
|
||||
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 .random(),
|
||||
uriPath: uriPath
|
||||
)
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Assertions
|
||||
|
||||
private extension DocCMiddlewareTests {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// 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.
|
||||
func assertInit(
|
||||
configuration: DocCMiddleware<LocalFileSystem>.Configuration,
|
||||
logger: Logger = .test()
|
||||
) {
|
||||
// GIVEN
|
||||
// WHEN
|
||||
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)
|
||||
|
||||
#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<FileSystemProvider: FileProvider>(
|
||||
configuration: DocCMiddleware<FileSystemProvider>.Configuration,
|
||||
logger: Logger = .test(),
|
||||
fileProvider: FileSystemProvider
|
||||
) {
|
||||
// GIVEN
|
||||
// WHEN
|
||||
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)
|
||||
|
||||
#expect(type(of:middleware.fileProvider) == FileSystemProvider.self)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// - 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)
|
||||
}
|
||||
}
|
||||
|
||||
/// Asserts a URI resource serving done by the middleware.
|
||||
/// - Parameters:
|
||||
/// - 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,
|
||||
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: 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"
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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())
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// 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 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.
|
||||
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"
|
||||
/// A URI path to use as a root sample.
|
||||
static let uriRoot = "/some/root/path"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// 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 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.
|
||||
@Tag static var useCase: Self
|
||||
|
||||
}
|
||||
@@ -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<Int>,
|
||||
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
|
||||
}
|
||||
@@ -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
|
||||
)}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 RequestContextMock {
|
||||
|
||||
// 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 RequestContextMock: RequestContext {
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
init(source: ApplicationRequestContextSource) {
|
||||
self.coreContext = .init(source: source)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
enum Input {}
|
||||
@@ -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 {}
|
||||
@@ -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<Int>,
|
||||
context: some RequestContext
|
||||
) async throws -> ResponseBody {
|
||||
.init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
Reference in New Issue
Block a user