Improved and documented the DocCModilleware middleware in the library target.

This commit is contained in:
2025-03-08 13:52:20 +01:00
parent 5b379fbc86
commit 980c8e15f6
5 changed files with 318 additions and 24 deletions
@@ -2,6 +2,15 @@ import Hummingbird
import Logging
import NIOPosix
/// A middleware that proxy requests to content inside `.doccarchive` archive containers located in a hosting app.
///
/// The routing logic this middleware implements are:
/// 1. Send all requests starting with `/documentation/` or `/tutorials/` to the `index.html` file;
/// 2. Send all requests starting with `/css/`, `/data/`, `/downloads/`, `/images/`, `/img/`, `/index/`, `/js/`, or `/videos/` to their respective folders;
/// 3. Send all requests to `favicon.ico`, `favicon.svg`, and `theme-settings.json` to their respective files;
/// 4. Send all requests to `/data/documentation.json` to the file in the `data/documentation/` folder that has the name of the module and ends with the `.json` extension;
/// 5. Redirect requests to `/` and `/documentation` to the`/documentation/` folder;
/// 6. Redirect requests to `/tutorials` to the`/tutorials/` folder.
struct DocCMiddleware<
Context: RequestContext,
AssetProvider: FileProvider
@@ -13,6 +22,11 @@ struct DocCMiddleware<
// MARK: Initialisers
/// Initialises this middleware with the local file system provider.
/// - Parameters:
/// - rootFolder: A root folder in the local file system where the *DocC* archive containers are located.
/// - threadPool: A thread pool used when loading archives from the file system.
/// - logger: A Logger that outputs information about the root folder requests.
init(
_ rootFolder: String,
threadPool: NIOThreadPool = .singleton,
@@ -24,7 +38,9 @@ struct DocCMiddleware<
logger: logger
)
}
/// Initialises this middleware with an asset provider, that conforms to the `FileProvider` protocol.
/// - Parameter assetProvider: An asset provider to use with the middleware.
init(assetProvider: AssetProvider) {
self.assetProvider = assetProvider
}
@@ -43,39 +59,33 @@ struct DocCMiddleware<
else {
throw HTTPError(.badRequest)
}
/*
Send all requests starting with /documentation/ or /tutorials/ to the index.html file
Send all requests starting with /css/, /data/, /downloads/, /images/, /img/, /index/, /js/, or /videos/ to their respective folders
Send all requests to favicon.ico, favicon.svg, and theme-settings.json to their respective files
Send all requests to /data/documentation.json to the file in the data/documentation/ folder that has the name of the module and ends with .json
Redirect requests to / and /documentation to /documentation/
Redirect requests to /tutorials to /tutorials/
*/
guard uriPath.starts(with: /^\/archives\/\w+/) else {
return try await next(input, context)
}
let pathArchive = Path.archivePath(from: uriPath)
let nameArchive = Path.archiveName(from: uriPath)
let uriResource = Path.resourcePath(from: uriPath)
let pathArchive = PathProvider.archivePath(from: uriPath)
let nameArchive = PathProvider.archiveName(from: uriPath)
let uriResource = PathProvider.resourcePath(from: uriPath)
// rule #5: Redirects URI resources with `/` to `/documentation`.
if uriResource == .forwardSlash {
return .redirect(to: uriPath + "/documentation")
return .redirect(to: String(format: .Format.Path.documentation, uriPath))
}
for staticFile in StaticFile.allCases {
if uriResource.contains(staticFile.path) {
if staticFile == .documentation {
// Send all requests to /data/documentation.json to the file in the data/documentation/ folder that has the name of the module and ends with .json
// Rule #4: Redirects URI resources with `/data/documentation.json` to the file in the `data/documentation/`
// folder that has the name of the module and ends with the `.json` extension in the *DocC* archive container.
return try await serveFile(
"/data/documentation/\(nameArchive).json",
String(format: .Format.Path.documentationJSON, nameArchive),
at: pathArchive,
context: context
)
} else {
// Send all requests to favicon.ico, favicon.svg, and theme-settings.json to their respective files
// Rule #3: Redirect URI resources for static files (`favicon.ico`, `favicon.svg`, and `theme-settings.json`)
// to their respective files in the *DocC* archive container.
return try await serveFile(
uriResource,
at: pathArchive,
@@ -87,6 +97,8 @@ struct DocCMiddleware<
for assetPrefix in AssetPrefix.allCases {
if uriResource.contains(assetPrefix.path) {
// Rule #2: Redirect URI resources for asset files (`/css/`, `/data/`, `/downloads/`, `/images/`, `/img/`,
// `/index/`, `/js/`, or `/videos/`)to their respective files in the *DocC* archive container.
return try await serveFile(
uriResource,
at: pathArchive,
@@ -97,19 +109,22 @@ struct DocCMiddleware<
for indexPrefix in IndexPrefix.allCases {
if uriResource.contains(indexPrefix.path) {
// Rule #1: Redirect URI resources for `/documentation/` and `/tutorials/` folders to their respective `index.html` file.
if uriResource.hasSuffix(.forwardSlash) {
return try await serveFile(
"\(indexPrefix.path)/\(nameArchive)/index.html",
String(format: .Format.Path.index, indexPrefix.path, nameArchive),
at: pathArchive,
context: context
)
} else {
return .redirect(to: uriPath + "/")
// rule #5: Redirects URI resources with `/documentation` to `/documentation/`.
// rule #6: Redirects URI resources with `/tutorials` to `/tutorials/`.
return .redirect(to: String(format: .Format.Path.forwardSlash, uriPath))
}
}
}
throw HTTPError(.notFound)
throw HTTPError(.notImplemented)
}
}
@@ -120,20 +135,29 @@ private extension DocCMiddleware {
// MARK: Functions
/// Serves a resource file from a provider as a HTTP response.
/// - Parameters:
/// - path: A relative path to a resource file.
/// - folder: A folder accessible to the provider where to find resource files.
/// - context: A request context.
/// - Returns: A HTTP response containing the content of a given resource file inside its body.
/// - Throws:An error...
func serveFile(
_ path: String,
at folder: String,
context: Context
) async throws -> Response {
let filePath = folder + path
guard let fileIdentifier = assetProvider.getFileIdentifier(filePath) else {
guard let fileIdentifier = assetProvider.getFileIdentifier(folder + path) else {
throw HTTPError(.notFound)
}
let body = try await assetProvider.loadFile(id: fileIdentifier, context: context)
return .init(status: .ok, headers: [:], body: body)
return .init(
status: .ok,
headers: [:],
body: body
)
}
}