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