diff --git a/Sources/HummingbirdDocC/Internal/Extensions/String+Helpers.swift b/Sources/HummingbirdDocC/Internal/Extensions/String+Helpers.swift index 8dfc996..dc00ff3 100644 --- a/Sources/HummingbirdDocC/Internal/Extensions/String+Helpers.swift +++ b/Sources/HummingbirdDocC/Internal/Extensions/String+Helpers.swift @@ -36,8 +36,11 @@ extension String { guard let matches = self.prefixMatch(of: pattern) else { return nil } - - return matches.output.1 ?? .empty + guard let subtracted = matches.output.1 else { + return .empty + } + + return subtracted } } diff --git a/Sources/HummingbirdDocC/Internal/Use Cases/PrepareURIPathUseCase.swift b/Sources/HummingbirdDocC/Internal/Use Cases/PrepareURIPathUseCase.swift index 9611a9b..d5f9b38 100644 --- a/Sources/HummingbirdDocC/Internal/Use Cases/PrepareURIPathUseCase.swift +++ b/Sources/HummingbirdDocC/Internal/Use Cases/PrepareURIPathUseCase.swift @@ -13,17 +13,12 @@ import Foundation import RegexBuilder -/// A use case that extracts data from a given URI path, essential for routing the documentation contents. +/// A use case that extracts a resource's information 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. + /// A root path that prefixes the documentation resource. private let uriRoot: String // MARK: Initializers @@ -39,44 +34,26 @@ struct PrepareURIPathUseCase { // 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. - /// + /// Extracts resource's information that is essential for documentation contents routing from a given 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 { + /// - Returns: A resource type containing all the relevant information + func callAsFunction(_ uriPath: String) -> Resource? { + guard let uriRest = uriRest(from: uriPath) else { return nil } - let archiveName = uriRest - .split(separator: .Path.forwardSlash) - .map(String.init) - .first + let archiveName = archiveName(from: uriRest) - let archiveReference: String = if let archiveName { - archiveName.lowercased() - } else { - .empty - } - let archivePath: String = if let archiveName { - .init(format: .Format.Path.archive, archiveName) - } else { - .empty + guard let relativePath = relativePath(from: uriRest, at: archiveName) else { + return nil } - return ( - archiveName ?? .empty, - archiveReference, - archivePath, - uriRest + return .init( + archiveName: archiveName, + relativePath: relativePath ) } @@ -88,40 +65,60 @@ private extension PrepareURIPathUseCase { // MARK: Functions + /// Extracts the archive name from a given URI path. + /// + /// > important: It is assumed that a given URI path is a relative URI path is prefixed with the `/` character and that contains the name of a `DocC` + /// documentation archive in its first component. + /// + /// - Parameter uriPath: A relative URI path to extract the archive name from. + /// - Returns: An archive named extracted from a given URI path. + func archiveName(from uriPath: String) -> String { + uriPath + .split(separator: .Path.forwardSlash) + .map(String.init) + .first ?? .empty + } + /// 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 { + func uriRest(from uriPath: String) -> String? { + guard let uriRest = uriPath.subtract(uriRoot) else { return nil } - guard let uriRest = matches.output.1 else { - return .Path.forwardSlash + guard !uriRest.isEmpty else { + return uriRest } guard uriRest.hasPrefix(.Path.forwardSlash) else { return .init(format: .Format.Path.root, uriRest) } + return uriRest } + /// Extracts the relative URI path from a given URI path against the name of a documentation archive. + /// - Parameters: + /// - uriPath: A URI path to get the relative URI path from. + /// - archive: An archive name to subtract from a URI path. + /// - Returns: A relative 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 relativePath( + from uriPath: String, + at archive: String + ) -> String? { + guard !archive.isEmpty else { + return .Path.forwardSlash + } + + let archivePath: String = .init(format: .Format.Path.root, archive) + + guard let relativePath = uriPath.subtract(archivePath) else { + return nil + } + guard relativePath != .empty else { + return .empty + } + + return relativePath + } + } diff --git a/Tests/HummingbirdDocC/Tests/Internal/Use Cases/PrepareURIPathUseCaseTests.swift b/Tests/HummingbirdDocC/Tests/Internal/Use Cases/PrepareURIPathUseCaseTests.swift index aa90000..53aafb2 100644 --- a/Tests/HummingbirdDocC/Tests/Internal/Use Cases/PrepareURIPathUseCaseTests.swift +++ b/Tests/HummingbirdDocC/Tests/Internal/Use Cases/PrepareURIPathUseCaseTests.swift @@ -12,6 +12,8 @@ import Testing +import struct HummingbirdDocC.Resource + @testable import struct HummingbirdDocC.PrepareURIPathUseCase @Suite("Prepare URI Path Use Case", .tags(.useCase)) @@ -26,7 +28,7 @@ struct PrepareURIPathUseCaseTests { )) func `extract data with URI root not suffixed with forward slash`( uri uriPath: String, - expects result: PrepareURIPathUseCase.PreparedURIPaths? + expects result: Resource? ) throws { try assertData( uriRoot: .uriRoot, @@ -41,7 +43,7 @@ struct PrepareURIPathUseCaseTests { )) func `extract data with URI root suffixed with forward slash`( uri uriPath: String, - expects result: PrepareURIPathUseCase.PreparedURIPaths? + expects result: Resource? ) throws { try assertData( uriRoot: .uriRootSlashed, @@ -56,7 +58,7 @@ struct PrepareURIPathUseCaseTests { )) func data_withURIRoot_notSuffixed_withForwardSlash( uri uriPath: String, - expects result: PrepareURIPathUseCase.PreparedURIPaths? + expects result: Resource? ) throws { try assertData( uriRoot: .uriRoot, @@ -71,7 +73,7 @@ struct PrepareURIPathUseCaseTests { )) func data_withURIRoot_suffixed_withForwardSlash( uri uriPath: String, - expects result: PrepareURIPathUseCase.PreparedURIPaths? + expects result: Resource? ) throws { try assertData( uriRoot: .uriRootSlashed, @@ -94,30 +96,20 @@ private extension PrepareURIPathUseCaseTests { /// - 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. + /// - resource: An expected resource coming out of the use case, if any. func assertData( uriRoot: String, uriPath: String, - expects result: PrepareURIPathUseCase.PreparedURIPaths? + expects resource: Resource? ) throws { // GIVEN let useCase = PrepareURIPathUseCase(uriRoot: uriRoot) // WHEN - let output = useCase(uriPath) + let result = 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) - } + #expect(result == resource) } } @@ -126,29 +118,29 @@ private extension PrepareURIPathUseCaseTests { 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] + static let prepareURIPaths: [String] = [.uriRoot, .uriOffset, .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] + static let prepareURIPathsSlashed: [String] = [.uriRootSlashed, .uriOffsetSlashed, .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), + static let prepareURIPaths: [Resource?] = [ + .init(archiveName: .empty, relativePath: .Path.forwardSlash), + .init(archiveName: "SomeArchive", relativePath: "/some/content/path"), nil ] } private extension String { /// A root URI path to initialize the use case with. - static let uriRoot: Self = "/some/path" + static let uriRoot = "/some/path" /// A root URI path suffixed with a forward slash to initialize the use case with. - static let uriRootSlashed: Self = "/some/path/" + static let uriRootSlashed = "/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" + static let uriOffset = .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" + static let uriOffsetSlashed = .uriRootSlashed + "SomeArchive/some/content/path" /// A URI path not related to any root URI path. - static let uriOther: Self = "/some/other/path" + static let uriOther = "/some/other/path" }