Compare commits

..

No commits in common. "main" and "v1.0.0" have entirely different histories.
main ... v1.0.0

17 changed files with 30 additions and 342 deletions

View File

@ -5,10 +5,9 @@ list of input parameters.
## Overview ## Overview
The ``Doxy`` service, in a nutshell, proxies the contents of each and every available `DocC` archive at a specific folder The service, in a nutshell, proxies the contents of each and every available `DocC` archive at a specific folder in the
in the local file system so they can be rendered by any web browser installed in the machine. Furthermore, it is also possible local file system so they can be rendered by any web browser installed in the machine. Furthermore, it is also possible
to query the list of the available `DocC` archives that are found at the *archives* folder in the local file system. Finally, to query the list of the available `DocC` archives that are found at the *archives* folder in the local file system.
the service could be configured by injecting ``Doxy/Options`` parameters as input parameters in the command-line interface.
The implementation that builds the service in this executable is provided by the `DoxyLibrary` target, that could be The implementation that builds the service in this executable is provided by the `DoxyLibrary` target, that could be
tested in total isolation. This is a standard pattern for [building command-line tools](https://www.swiftbysundell.com/articles/building-a-command-line-tool-using-the-swift-package-manager/) tested in total isolation. This is a standard pattern for [building command-line tools](https://www.swiftbysundell.com/articles/building-a-command-line-tool-using-the-swift-package-manager/)
@ -16,11 +15,7 @@ with the [Swift programming language](https://www.swift.org).
## Topics ## Topics
### Essentials ### Executable
- <doc:DoxyInstallation>
### Service
- ``Doxy`` - ``Doxy``

View File

@ -1,13 +0,0 @@
@Tutorials(name: "Doxy tutorials") {
@Intro(title: "Doxy service tutotials") {
These tutorials explains how to install the **Doxy** service, and to use it afterwards; from any command-line
interface, such as the *Terminal* app that is shipped with any version of *MacOS*.
}
@Chapter(name: "Installation") {
This tutorial explains how to install the **Doxy** service into the system from the command-line interface of
choice for the developer.
@TutorialReference(tutorial: "doc:DoxyInstallation")
}
}

View File

@ -2,10 +2,6 @@
## Topics ## Topics
### Essentials
- <doc:DoxyInstallation>
### Input parameters ### Input parameters
- ``Doxy/Options`` - ``Doxy/Options``

View File

@ -1,71 +0,0 @@
@Tutorial(time: 5) {
@Intro(title: "Install the Doxy service in the machine") {
This tutorial explains step-by-step how to obtain the **Doxy** service from a remote repository and also, how
to install this service into a system afterwards, so it would be ready to use by the developer.
}
@Section(title: "Cloning the Doxy service") {
@ContentAndMedia {
This section focuses on how to clone (or to obtain) the **Doxy** service from its remote repository into the
system, so the service is ready to be installed in the machine.
The `git` command line tool, that is included into the *Command Line tools for Xcode* package, is therefore
required to clone the repository. If the command is not already installed in the machine, then download the
mentioned package from the [Downloads section in the Apple Developer website](https://developer.apple.com/download/all/).
}
@Steps {
@Step {
Check whether the `git` command is available to use in the command-line interface of choice.
In case an absolute path is returned after the command is run, means that the command is installed in
the machine. Otherwise, then it is required to install the *Command Line Tools for Xcode* in the machine
from [the downloads section of the Apple Developer website]((https://developer.apple.com/download/all/)).
@Code(name: "Check for the git command in the machine", file: "service_installation-1_1.bash")
}
@Step {
Clone the **Doxy** service from a remote repository into the machine using the `git` command.
@Code(name: "Clone the remote repository into the machine", file: "service_installation-1_2.bash")
}
@Step {
Change the current directory to the cloned (or downloaded) folder that contains the source code of the
**Doxy** service application.
@Code(name: "Change the current directory to the cloned folder", file: "service_installation-1_3.bash")
}
}
}
@Section(title: "Installing the Doxy service") {
@ContentAndMedia {
This section focuses on installing the **Doxy** command-line tool from its source code, that has been cloned
(or downloaded) from the remote repository in the previous section of this tutorial.
}
@Steps {
@Step {
Make a release build and then install the generated command-line executable into the machine's user binary
location, to allow the developer to use this command easily.
In case the developer does not have *admin* or *root* privileges, then it is required to copy the generated
executable to some folder within the machine manually.
@Code(name: "Install the service into the machine", file: "service_installation-2_1.bash")
}
@Step {
Finally it is recommendable to test whether the executable has been installed in the machine or that the
generated executable works as expected.
For this purpose, the *help* subcommand is called as a parameter
with the command-line tool.
@Code(name: "Test the service has been installed in the machine", file: "service_installation-2_2.bash")
}
}
}
}

View File

@ -1 +0,0 @@
$ git clone https://repo.rock-n-code.com/rock-n-code/doxy.git .

View File

@ -1,3 +0,0 @@
$ make install
# Or manually copy the executable to a custom folder
$ cp .build/release/doxy /path/to/some/folder

View File

@ -1,3 +0,0 @@
$ doxy --help
# In case the executable has been manually copied to some folder.
$ ./path/to/some/folder/doxy --help

View File

@ -18,7 +18,7 @@ extension Doxy {
/// A hostname to bind the application to. /// A hostname to bind the application to.
@Option( @Option(
name: .long, name: .long,
help: "A hostname to bind the application to." help: "A hostname to bind the application to. Defaults to `127.0.0.1`."
) )
var hostname: String = "127.0.0.1" var hostname: String = "127.0.0.1"
@ -32,14 +32,14 @@ extension Doxy {
/// A name for the Hummingbird application. /// A name for the Hummingbird application.
@Option( @Option(
name: .long, name: .long,
help: "A name for the Hummingbird application." help: "A name for the Hummingbird application. Defaults to `Doxy`."
) )
var name: String = "Doxy" var name: String = "Doxy"
/// A port number to bind the application to. /// A port number to bind the application to.
@Option( @Option(
name: .long, name: .long,
help: "A port number to bind the application to." help: "A port number to bind the application to. Defaults to `8080`."
) )
var port: Int = 8080 var port: Int = 8080

View File

@ -4,17 +4,13 @@ A library that provides the **Doxy** service builder to the `DoxyApp` executable
## Overview ## Overview
The library, in a nutshell, provides a public ``AppBuilder`` builder that creates a fully functional [Hummingbird](https://hummingbird.codes) The library, in a nutshell, provides a public builder that creates a fully configured [Hummingbird](https://hummingbird.codes)
application that is ready-to-use by the `DoxyApp` executable target. Furthermore, this application can be configured application that is ready-to-use by the `DoxyApp` executable target.
with the ``AppArguments`` parameters the executable receives as inputs.
In addition, all the business logic required by the ``ArchiveController`` controller that provides the list of available In addition, all the business logic required by the `DocC` archives controller that provides the necessary resources,
`DocC` archives at an specific location in the local file system, and the ``DocCMiddleware`` middleware that proxies the and the `DocC` middleware that proxies the contents of documentation archives in the local machine is being implemented
contents of requested documentation archives in the local machine have been implemented in this target, including all its in this target, including all its relevant protocols and types. Plus, the unit tests for every type implemented in this
relevant protocols and types. target are being written in the `DoxyTests` target.
Plus, the test cases for every type found in this target have been written in the `DoxyTests` unit tests target using the
new [Swift Testing](https://developer.apple.com/xcode/swift-testing/) testing framework.
## Topics ## Topics

View File

@ -1,5 +1,4 @@
import Hummingbird import Hummingbird
import Logging
/// A controller type that provides information about the *DocC* archives containers. /// A controller type that provides information about the *DocC* archives containers.
struct ArchiveController<Context: RequestContext> { struct ArchiveController<Context: RequestContext> {
@ -8,7 +7,6 @@ struct ArchiveController<Context: RequestContext> {
private let archivesPath: String private let archivesPath: String
private let fileService: any FileServicing private let fileService: any FileServicing
private let logger: Logger
// MARK: Initialisers // MARK: Initialisers
@ -16,15 +14,12 @@ struct ArchiveController<Context: RequestContext> {
/// - Parameters: /// - Parameters:
/// - archivesPath: A path in the local file system where the *DocC* archive contained are located. /// - archivesPath: A path in the local file system where the *DocC* archive contained are located.
/// - fileService: A service that interfaces with the local file system. /// - fileService: A service that interfaces with the local file system.
/// - logger: A service that interfaces with the logging system.
init( init(
_ archivesPath: String, _ archivesPath: String,
fileService: any FileServicing = FileService(), fileService: any FileServicing = FileService()
logger: Logger
) { ) {
self.archivesPath = archivesPath self.archivesPath = archivesPath
self.fileService = fileService self.fileService = fileService
self.logger = logger
} }
// MARK: Functions // MARK: Functions
@ -54,62 +49,13 @@ private extension ArchiveController {
.map { $0.dropLast(String.suffixArchive.count) } .map { $0.dropLast(String.suffixArchive.count) }
.map(String.init) .map(String.init)
.sorted { $0 < $1 } .sorted { $0 < $1 }
let archiveList: ArchiveList = .init(nameArchives)
return .init(nameArchives)
defer {
logger.debug(
"A codable response returned: \(String(describing: archiveList))",
metadata: .metadata(
context: context,
request: request,
statusCode: .ok
),
source: .source
)
}
return archiveList
} catch .folderNotFound { } catch .folderNotFound {
defer {
logger.error(
"The resource has not been found.",
metadata: .metadata(
context: context,
request: request,
statusCode: .notFound
),
source: .source
)
}
throw .init(.notFound) throw .init(.notFound)
} catch .folderPathEmpty, .folderNotDirectory { } catch .folderPathEmpty, .folderNotDirectory {
defer { throw .init(.unprocessableContent)
logger.error(
"The folder of the resource has not been located.",
metadata: .metadata(
context: context,
request: request,
statusCode: .notAcceptable
),
source: .source
)
}
throw .init(.notAcceptable)
} catch { } catch {
defer {
logger.error(
"The request has issues.",
metadata: .metadata(
context: context,
request: request,
statusCode: .badRequest
),
source: .source
)
}
throw .init(.badRequest) throw .init(.badRequest)
} }
} }
@ -125,6 +71,5 @@ private extension RouterPath {
// MARK: - String+Constants // MARK: - String+Constants
private extension String { private extension String {
static let source = "ArchiveController"
static let suffixArchive: String = ".doccarchive" static let suffixArchive: String = ".doccarchive"
} }

View File

@ -1,28 +0,0 @@
import Hummingbird
import Logging
extension Logger.Metadata {
// MARK: Functions
static func metadata<Context: RequestContext>(
context: Context,
request: Request,
statusCode: HTTPResponse.Status,
redirect: String? = nil
) -> Logger.Metadata {
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
}
}

View File

@ -19,7 +19,6 @@ struct DocCMiddleware<
// MARK: Properties // MARK: Properties
private let assetProvider: AssetProvider private let assetProvider: AssetProvider
private let logger: Logger
// MARK: Initialisers // MARK: Initialisers
@ -27,30 +26,23 @@ struct DocCMiddleware<
/// - Parameters: /// - Parameters:
/// - rootFolder: A root folder in the local file system where the *DocC* archive containers are located. /// - 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. /// - threadPool: A thread pool used when loading archives from the file system.
/// - logger: A service that interacts with the logging system, /// - logger: A Logger that outputs information about the root folder requests.
init( init(
_ rootFolder: String, _ rootFolder: String,
threadPool: NIOThreadPool = .singleton, threadPool: NIOThreadPool = .singleton,
logger: Logger logger: Logger = .init(label: "DocCMiddleware")
) where AssetProvider == LocalFileSystem { ) where AssetProvider == LocalFileSystem {
self.assetProvider = LocalFileSystem( self.assetProvider = LocalFileSystem(
rootFolder: rootFolder, rootFolder: rootFolder,
threadPool: threadPool, threadPool: threadPool,
logger: logger logger: logger
) )
self.logger = logger
} }
/// Initialises this middleware with an asset provider, that conforms to the `FileProvider` protocol. /// Initialises this middleware with an asset provider, that conforms to the `FileProvider` protocol.
/// - Parameters: /// - Parameter assetProvider: An asset provider to use with the middleware.
/// - assetProvider: An asset provider to use with the middleware. init(assetProvider: AssetProvider) {
/// - logger: A service that interacts with the logging system,
init(
assetProvider: AssetProvider,
logger: Logger
) {
self.assetProvider = assetProvider self.assetProvider = assetProvider
self.logger = logger
} }
// MARK: Functions // MARK: Functions
@ -65,18 +57,6 @@ struct DocCMiddleware<
!uriPath.contains(.previousFolder), !uriPath.contains(.previousFolder),
uriPath.hasPrefix(.forwardSlash) uriPath.hasPrefix(.forwardSlash)
else { else {
defer {
logger.error(
"The request has issues.",
metadata: .metadata(
context: context,
request: input,
statusCode: .badRequest
),
source: .source
)
}
throw HTTPError(.badRequest) throw HTTPError(.badRequest)
} }
@ -90,17 +70,11 @@ struct DocCMiddleware<
// rule #5: Redirects URI resources with `/` to `/documentation`. // rule #5: Redirects URI resources with `/` to `/documentation`.
if uriResource == .forwardSlash { if uriResource == .forwardSlash {
let pathRedirect = if uriPath.hasSuffix(.forwardSlash) { return if uriPath.hasSuffix(.forwardSlash) {
String(format: .Format.Path.documentation, uriPath) .redirect(to: String(format: .Format.Path.documentation, uriPath))
} else { } else {
String(format: .Format.Path.forwardSlash, uriPath) .redirect(to: String(format: .Format.Path.forwardSlash, uriPath))
} }
return redirect(
to: pathRedirect,
input: input,
context: context
)
} }
for staticFile in StaticFile.allCases { for staticFile in StaticFile.allCases {
@ -111,7 +85,6 @@ struct DocCMiddleware<
return try await serveFile( return try await serveFile(
String(format: .Format.Path.documentationJSON, nameArchive), String(format: .Format.Path.documentationJSON, nameArchive),
at: pathArchive, at: pathArchive,
input: input,
context: context context: context
) )
} else { } else {
@ -120,7 +93,6 @@ struct DocCMiddleware<
return try await serveFile( return try await serveFile(
uriResource, uriResource,
at: pathArchive, at: pathArchive,
input: input,
context: context context: context
) )
} }
@ -134,7 +106,6 @@ struct DocCMiddleware<
return try await serveFile( return try await serveFile(
uriResource, uriResource,
at: pathArchive, at: pathArchive,
input: input,
context: context context: context
) )
} }
@ -147,32 +118,15 @@ struct DocCMiddleware<
return try await serveFile( return try await serveFile(
String(format: .Format.Path.index, indexPrefix.path, nameArchive), String(format: .Format.Path.index, indexPrefix.path, nameArchive),
at: pathArchive, at: pathArchive,
input: input,
context: context context: context
) )
} else { } else {
// rule #5: Redirects URI resources with `/documentation` to `/documentation/`. // rule #5: Redirects URI resources with `/documentation` to `/documentation/`.
// rule #6: Redirects URI resources with `/tutorials` to `/tutorials/`. // rule #6: Redirects URI resources with `/tutorials` to `/tutorials/`.
return redirect( return .redirect(to: String(format: .Format.Path.forwardSlash, uriPath))
to: String(format: .Format.Path.forwardSlash, uriPath),
input: input,
context: context
)
} }
} }
} }
defer {
logger.error(
"The request has not been implemented yet.",
metadata: .metadata(
context: context,
request: input,
statusCode: .notImplemented
),
source: .source
)
}
throw HTTPError(.notImplemented) throw HTTPError(.notImplemented)
} }
@ -185,79 +139,23 @@ private extension DocCMiddleware {
// MARK: Functions // MARK: Functions
/// Redirects a request to a new relative path.
/// - Parameters:
/// - path: A relative path to use in the redirection.
/// - input: An input request.
/// - context: A request context.
/// - Returns: A HTTP response containing the redirection to another
func redirect(
to path: String,
input: Request,
context: Context
) -> Response {
defer {
logger.debug(
"The path URI has been redirected to: \(path)",
metadata: .metadata(
context: context,
request: input,
statusCode: .permanentRedirect,
redirect: path
),
source: .source
)
}
return .redirect(to: path)
}
/// Serves a resource file from a provider as a HTTP response. /// Serves a resource file from a provider as a HTTP response.
/// - Parameters: /// - Parameters:
/// - path: A relative path to a resource file. /// - path: A relative path to a resource file.
/// - folder: A folder accessible to the provider where to find resource files. /// - folder: A folder accessible to the provider where to find resource files.
/// - input: An input request.
/// - context: A request context. /// - context: A request context.
/// - Returns: A HTTP response containing the content of a given resource file inside its body. /// - Returns: A HTTP response containing the content of a given resource file inside its body.
/// - Throws:An error... /// - Throws:An error...
func serveFile( func serveFile(
_ path: String, _ path: String,
at folder: String, at folder: String,
input: Request,
context: Context context: Context
) async throws -> Response { ) async throws -> Response {
guard let fileIdentifier = assetProvider.getFileIdentifier(folder + path) else { guard let fileIdentifier = assetProvider.getFileIdentifier(folder + path) else {
defer {
logger.error(
"The resource has not been found.",
metadata: .metadata(
context: context,
request: input,
statusCode: .notFound
),
source: .source
)
}
throw HTTPError(.notFound) throw HTTPError(.notFound)
} }
let body = try await assetProvider.loadFile( let body = try await assetProvider.loadFile(id: fileIdentifier, context: context)
id: fileIdentifier,
context: context
)
defer {
logger.debug(
"The body of the response returned: \(body.contentLength ?? 0) bytes.",
metadata: .metadata(
context: context,
request: input,
statusCode: .ok
),
source: .source
)
}
return .init( return .init(
status: .ok, status: .ok,
@ -267,9 +165,3 @@ private extension DocCMiddleware {
} }
} }
// MARK: - String+Constants
private extension String {
static let source = "DocCMiddleware"
}

View File

@ -79,14 +79,10 @@ private extension AppBuilder {
router.addMiddleware { router.addMiddleware {
LogRequestsMiddleware(logger.logLevel) LogRequestsMiddleware(logger.logLevel)
DocCMiddleware(archivesPath, logger: logger) DocCMiddleware(archivesPath)
} }
ArchiveController( ArchiveController(archivesPath).register(to: router)
archivesPath,
logger: logger
)
.register(to: router)
return router return router
} }

View File

@ -36,8 +36,8 @@ update: ## Updates the SPM package dependencies.
build: ## Builds the project locally. build: ## Builds the project locally.
@swift build -c release @swift build -c release
run: build ## Runs the project locally. run: ## Runs the project locally.
@swift run doxy --archives-path "$(DOCC_ARCHIVES_FOLDER)" --log-level debug @swift run -c release
tests: ## Runs all the test cases of the project. tests: ## Runs all the test cases of the project.
@swift test --enable-swift-testing @swift test --enable-swift-testing

View File

@ -44,17 +44,6 @@ $ make install
$ doxy --help $ doxy --help
``` ```
If a `permission issue` issue is raised when installing the built executable into the system binaries folder, means the user used have no *admin* and/or *root* priviledges. In that case, the developer should copy/install the built executable manually to a custom location in the file system.
```bash
# Copy the built executable into a non-protected folder.
$ cp ./build/release/doxy /path/to/some/folder
# Change the current folder to the non-protected folder.
$ cd /path/to/some/folder
# Make sure the executable is working
$ ./doxy --help
```
### Usage ### Usage
To use this service, please do execute the following commands in the prompt of the **Terminal** command-line app in the local system: To use this service, please do execute the following commands in the prompt of the **Terminal** command-line app in the local system: