From 19a54b25ae504190461b55d809c8b8f0ce4effb8 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 25 Feb 2025 01:55:49 +0100 Subject: [PATCH] Implemented (a first version of) the DocCMiddleware middleware in the library target. --- .../Internal/Enumerations/AssetPrefix.swift | 22 +++ .../Internal/Enumerations/IndexPrefix.swift | 16 ++ .../Internal/Enumerations/StaticFile.swift | 21 +++ .../Internal/Extensions/Path+Functions.swift | 41 ++++++ .../Internal/Middlewares/DocCMiddleware.swift | 138 ++++++++++++++++++ .../Sources/Internal/Protocols/Pathable.swift | 16 ++ 6 files changed, 254 insertions(+) create mode 100644 Library/Sources/Internal/Enumerations/AssetPrefix.swift create mode 100644 Library/Sources/Internal/Enumerations/IndexPrefix.swift create mode 100644 Library/Sources/Internal/Enumerations/StaticFile.swift create mode 100644 Library/Sources/Internal/Extensions/Path+Functions.swift create mode 100644 Library/Sources/Internal/Middlewares/DocCMiddleware.swift create mode 100644 Library/Sources/Internal/Protocols/Pathable.swift diff --git a/Library/Sources/Internal/Enumerations/AssetPrefix.swift b/Library/Sources/Internal/Enumerations/AssetPrefix.swift new file mode 100644 index 0000000..6dff450 --- /dev/null +++ b/Library/Sources/Internal/Enumerations/AssetPrefix.swift @@ -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) + } + +} diff --git a/Library/Sources/Internal/Enumerations/IndexPrefix.swift b/Library/Sources/Internal/Enumerations/IndexPrefix.swift new file mode 100644 index 0000000..87ecb40 --- /dev/null +++ b/Library/Sources/Internal/Enumerations/IndexPrefix.swift @@ -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) + } + +} diff --git a/Library/Sources/Internal/Enumerations/StaticFile.swift b/Library/Sources/Internal/Enumerations/StaticFile.swift new file mode 100644 index 0000000..4bae878 --- /dev/null +++ b/Library/Sources/Internal/Enumerations/StaticFile.swift @@ -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) + } + } + +} diff --git a/Library/Sources/Internal/Extensions/Path+Functions.swift b/Library/Sources/Internal/Extensions/Path+Functions.swift new file mode 100644 index 0000000..2ad1ed1 --- /dev/null +++ b/Library/Sources/Internal/Extensions/Path+Functions.swift @@ -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" +} diff --git a/Library/Sources/Internal/Middlewares/DocCMiddleware.swift b/Library/Sources/Internal/Middlewares/DocCMiddleware.swift new file mode 100644 index 0000000..fa46670 --- /dev/null +++ b/Library/Sources/Internal/Middlewares/DocCMiddleware.swift @@ -0,0 +1,138 @@ +import Hummingbird +import Logging +import NIOPosix + +struct DocCMiddleware: 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) + } + +} diff --git a/Library/Sources/Internal/Protocols/Pathable.swift b/Library/Sources/Internal/Protocols/Pathable.swift new file mode 100644 index 0000000..ba37ef1 --- /dev/null +++ b/Library/Sources/Internal/Protocols/Pathable.swift @@ -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 = "/%@" + } +}