Implemented (a first version of) the DocCMiddleware middleware in the library target.

This commit is contained in:
Javier Cicchelli 2025-02-25 01:55:49 +01:00
parent 54aa5c70c6
commit 19a54b25ae
6 changed files with 254 additions and 0 deletions

View File

@ -0,0 +1,22 @@
enum AssetPrefix: String, CaseIterable {
case css
case data
case downloads
case images
case img
case index
case js
case videos
}
// MARK: - Pathable
extension AssetPrefix: Pathable {
// MARK: Computed
var path: String {
.init(format: .Format.pathRoot, rawValue)
}
}

View File

@ -0,0 +1,16 @@
enum IndexPrefix: String, CaseIterable {
case documentation
case tutorials
}
// MARK: - Pathable
extension IndexPrefix: Pathable {
// MARK: Computed
var path: String {
.init(format: .Format.pathRoot, rawValue)
}
}

View File

@ -0,0 +1,21 @@
enum StaticFile: String, CaseIterable {
case documentation = "documentation.json"
case faviconICO = "favicon.ico"
case faviconSVG = "favicon.svg"
case themeSettings = "theme-settings.json"
}
// MARK: - Pathable
extension StaticFile: Pathable {
// MARK: Computed
var path: String {
switch self {
case .documentation: "/data/" + rawValue
default: .init(format: .Format.pathRoot, rawValue)
}
}
}

View File

@ -0,0 +1,41 @@
enum Path {
// MARK: Functions
static func archiveName(from path: String) -> String {
let pathComponents = path.split(separator: .forwardSlash)
return pathComponents.count > 1
? String(pathComponents[1]).lowercased()
: .empty
}
static func resourcePath(from path: String) -> String {
let matches = path.matches(of: /\//)
return matches.count > 2
? .init(path[matches[2].startIndex...])
: .forwardSlash
}
}
// MARK: - String+Constants
extension String {
static let empty = ""
static let forwardSlash = "/"
static let previousFolder = ".."
}
// MARK: - Character+Constants
private extension Character {
static let forwardSlash: Character = "/"
}
// MARK: - String+Formats
private extension String.Format {
static let archiveDocC = "%@.doccarchive"
}

View File

@ -0,0 +1,138 @@
import Hummingbird
import Logging
import NIOPosix
struct DocCMiddleware<Context: RequestContext, AssetProvider: FileProvider>: RouterMiddleware {
// MARK: Properties
private let assetProvider: AssetProvider
// MARK: Initialisers
init(
_ rootFolder: String,
threadPool: NIOThreadPool = .singleton,
logger: Logger = .init(label: "DocCMiddleware")
) where AssetProvider == LocalFileSystem {
self.assetProvider = LocalFileSystem(
rootFolder: rootFolder,
threadPool: threadPool,
logger: logger
)
}
init(assetProvider: AssetProvider) {
self.assetProvider = assetProvider
}
// MARK: Functions
func handle(
_ input: Request,
context: Context,
next: (Request, Context) async throws -> Response
) async throws -> Response {
guard
let uriPath = input.uri.path.removingPercentEncoding,
!uriPath.contains(.previousFolder),
uriPath.hasPrefix(.forwardSlash)
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/
*/
print(uriPath)
guard uriPath.starts(with: /^\/docs\/\w+/) else {
return try await next(input, context)
}
let nameArchive = Path.archiveName(from: uriPath)
let uriResource = Path.resourcePath(from: uriPath)
print(nameArchive)
print(uriResource)
if uriResource == .forwardSlash {
return .redirect(to: uriPath + "/documentation")
}
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
return try await serveFile("/data/documentation/\(nameArchive).json", at: "/docs", context: context)
} else {
// Send all requests to favicon.ico, favicon.svg, and theme-settings.json to their respective files
return try await serveFile(uriResource, at: "/docs", context: context)
}
}
}
for assetPrefix in AssetPrefix.allCases {
if uriResource.contains(assetPrefix.path) {
return try await serveFile(uriResource, at: "/docs", context: context)
}
}
for indexPrefix in IndexPrefix.allCases {
if uriResource.contains(indexPrefix.path) {
if uriResource.hasSuffix(.forwardSlash) {
return try await serveFile(
"/documentation/\(nameArchive)/index.html",
at: "/docs",
context: context
)
} else {
return .redirect(to: uriPath + "/")
}
}
}
return .init(status: .ok)
}
}
// MARK: - Helpers
private extension DocCMiddleware {
// MARK: Functions
func serveFile(
_ path: String,
at folder: String? = nil,
context: Context
) async throws -> Response {
let filePath = if let folder {
folder + path
} else {
path
}
print(filePath)
guard let fileIdentifier = assetProvider.getFileIdentifier(filePath) else {
throw HTTPError(.notFound)
}
let body = try await assetProvider.loadFile(id: fileIdentifier, context: context)
print(fileIdentifier)
print(body)
return .init(status: .ok, headers: [:], body: body)
}
}

View File

@ -0,0 +1,16 @@
protocol Pathable {
// MARK: Properties
var path: String { get }
}
// MARK: - String+Formats
extension String {
enum Format {
static let pathDocs = "/docs/%@"
static let pathRoot = "/%@"
}
}