Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
6dad51c027 | |||
e81516cc5d | |||
c35c81413c | |||
de2e2dd656 | |||
fd7dd9bd01 | |||
42758f29c1 | |||
469af173cf | |||
e3728e943f | |||
7f08a45cc8 | |||
dcc0b509c2 | |||
382dabeda8 | |||
2d64d832c0 | |||
f610fa9a2b | |||
fff2be3a50 |
@ -5,9 +5,10 @@ list of input parameters.
|
||||
|
||||
## Overview
|
||||
|
||||
The service, in a nutshell, proxies the contents of each and every available `DocC` archive at a specific folder in the
|
||||
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.
|
||||
The ``Doxy`` service, in a nutshell, proxies the contents of each and every available `DocC` archive at a specific folder
|
||||
in the 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,
|
||||
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
|
||||
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/)
|
||||
@ -15,7 +16,11 @@ with the [Swift programming language](https://www.swift.org).
|
||||
|
||||
## Topics
|
||||
|
||||
### Executable
|
||||
### Essentials
|
||||
|
||||
- <doc:DoxyInstallation>
|
||||
|
||||
### Service
|
||||
|
||||
- ``Doxy``
|
||||
|
||||
|
13
App/Catalogs/Doxy.docc/DoxyTutorials.tutorial
Normal file
13
App/Catalogs/Doxy.docc/DoxyTutorials.tutorial
Normal file
@ -0,0 +1,13 @@
|
||||
@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")
|
||||
}
|
||||
}
|
@ -2,6 +2,10 @@
|
||||
|
||||
## Topics
|
||||
|
||||
### Essentials
|
||||
|
||||
- <doc:DoxyInstallation>
|
||||
|
||||
### Input parameters
|
||||
|
||||
- ``Doxy/Options``
|
||||
|
71
App/Catalogs/Doxy.docc/Tutorials/DoxyInstallation.tutorial
Normal file
71
App/Catalogs/Doxy.docc/Tutorials/DoxyInstallation.tutorial
Normal file
@ -0,0 +1,71 @@
|
||||
@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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
$ which git
|
@ -0,0 +1 @@
|
||||
$ git clone https://repo.rock-n-code.com/rock-n-code/doxy.git .
|
@ -0,0 +1 @@
|
||||
$ cd doxy
|
@ -0,0 +1,3 @@
|
||||
$ make install
|
||||
# Or manually copy the executable to a custom folder
|
||||
$ cp .build/release/doxy /path/to/some/folder
|
@ -0,0 +1,3 @@
|
||||
$ doxy --help
|
||||
# In case the executable has been manually copied to some folder.
|
||||
$ ./path/to/some/folder/doxy --help
|
@ -18,7 +18,7 @@ extension Doxy {
|
||||
/// A hostname to bind the application to.
|
||||
@Option(
|
||||
name: .long,
|
||||
help: "A hostname to bind the application to. Defaults to `127.0.0.1`."
|
||||
help: "A hostname to bind the application to."
|
||||
)
|
||||
var hostname: String = "127.0.0.1"
|
||||
|
||||
@ -32,14 +32,14 @@ extension Doxy {
|
||||
/// A name for the Hummingbird application.
|
||||
@Option(
|
||||
name: .long,
|
||||
help: "A name for the Hummingbird application. Defaults to `Doxy`."
|
||||
help: "A name for the Hummingbird application."
|
||||
)
|
||||
var name: String = "Doxy"
|
||||
|
||||
/// A port number to bind the application to.
|
||||
@Option(
|
||||
name: .long,
|
||||
help: "A port number to bind the application to. Defaults to `8080`."
|
||||
help: "A port number to bind the application to."
|
||||
)
|
||||
var port: Int = 8080
|
||||
|
||||
|
@ -4,13 +4,17 @@ A library that provides the **Doxy** service builder to the `DoxyApp` executable
|
||||
|
||||
## Overview
|
||||
|
||||
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.
|
||||
The library, in a nutshell, provides a public ``AppBuilder`` builder that creates a fully functional [Hummingbird](https://hummingbird.codes)
|
||||
application that is ready-to-use by the `DoxyApp` executable target. Furthermore, this application can be configured
|
||||
with the ``AppArguments`` parameters the executable receives as inputs.
|
||||
|
||||
In addition, all the business logic required by the `DocC` archives controller that provides the necessary resources,
|
||||
and the `DocC` middleware that proxies the contents of documentation archives in the local machine is being implemented
|
||||
in this target, including all its relevant protocols and types. Plus, the unit tests for every type implemented in this
|
||||
target are being written in the `DoxyTests` target.
|
||||
In addition, all the business logic required by the ``ArchiveController`` controller that provides the list of available
|
||||
`DocC` archives at an specific location in the local file system, and the ``DocCMiddleware`` middleware that proxies the
|
||||
contents of requested documentation archives in the local machine have been implemented in this target, including all its
|
||||
relevant protocols and types.
|
||||
|
||||
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
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import Hummingbird
|
||||
import Logging
|
||||
|
||||
/// A controller type that provides information about the *DocC* archives containers.
|
||||
struct ArchiveController<Context: RequestContext> {
|
||||
@ -7,6 +8,7 @@ struct ArchiveController<Context: RequestContext> {
|
||||
|
||||
private let archivesPath: String
|
||||
private let fileService: any FileServicing
|
||||
private let logger: Logger
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
@ -14,12 +16,15 @@ struct ArchiveController<Context: RequestContext> {
|
||||
/// - Parameters:
|
||||
/// - 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.
|
||||
/// - logger: A service that interfaces with the logging system.
|
||||
init(
|
||||
_ archivesPath: String,
|
||||
fileService: any FileServicing = FileService()
|
||||
fileService: any FileServicing = FileService(),
|
||||
logger: Logger
|
||||
) {
|
||||
self.archivesPath = archivesPath
|
||||
self.fileService = fileService
|
||||
self.logger = logger
|
||||
}
|
||||
|
||||
// MARK: Functions
|
||||
@ -49,13 +54,62 @@ private extension ArchiveController {
|
||||
.map { $0.dropLast(String.suffixArchive.count) }
|
||||
.map(String.init)
|
||||
.sorted { $0 < $1 }
|
||||
|
||||
return .init(nameArchives)
|
||||
let archiveList: ArchiveList = .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 {
|
||||
defer {
|
||||
logger.error(
|
||||
"The resource has not been found.",
|
||||
metadata: .metadata(
|
||||
context: context,
|
||||
request: request,
|
||||
statusCode: .notFound
|
||||
),
|
||||
source: .source
|
||||
)
|
||||
}
|
||||
|
||||
throw .init(.notFound)
|
||||
} catch .folderPathEmpty, .folderNotDirectory {
|
||||
throw .init(.unprocessableContent)
|
||||
defer {
|
||||
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 {
|
||||
defer {
|
||||
logger.error(
|
||||
"The request has issues.",
|
||||
metadata: .metadata(
|
||||
context: context,
|
||||
request: request,
|
||||
statusCode: .badRequest
|
||||
),
|
||||
source: .source
|
||||
)
|
||||
}
|
||||
|
||||
throw .init(.badRequest)
|
||||
}
|
||||
}
|
||||
@ -71,5 +125,6 @@ private extension RouterPath {
|
||||
// MARK: - String+Constants
|
||||
|
||||
private extension String {
|
||||
static let source = "ArchiveController"
|
||||
static let suffixArchive: String = ".doccarchive"
|
||||
}
|
||||
|
@ -0,0 +1,28 @@
|
||||
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
|
||||
}
|
||||
|
||||
}
|
@ -19,6 +19,7 @@ struct DocCMiddleware<
|
||||
// MARK: Properties
|
||||
|
||||
private let assetProvider: AssetProvider
|
||||
private let logger: Logger
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
@ -26,23 +27,30 @@ struct DocCMiddleware<
|
||||
/// - 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.
|
||||
/// - logger: A service that interacts with the logging system,
|
||||
init(
|
||||
_ rootFolder: String,
|
||||
threadPool: NIOThreadPool = .singleton,
|
||||
logger: Logger = .init(label: "DocCMiddleware")
|
||||
logger: Logger
|
||||
) where AssetProvider == LocalFileSystem {
|
||||
self.assetProvider = LocalFileSystem(
|
||||
rootFolder: rootFolder,
|
||||
threadPool: threadPool,
|
||||
logger: logger
|
||||
)
|
||||
self.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) {
|
||||
/// - Parameters:
|
||||
/// - assetProvider: An asset provider to use with the middleware.
|
||||
/// - logger: A service that interacts with the logging system,
|
||||
init(
|
||||
assetProvider: AssetProvider,
|
||||
logger: Logger
|
||||
) {
|
||||
self.assetProvider = assetProvider
|
||||
self.logger = logger
|
||||
}
|
||||
|
||||
// MARK: Functions
|
||||
@ -57,6 +65,18 @@ struct DocCMiddleware<
|
||||
!uriPath.contains(.previousFolder),
|
||||
uriPath.hasPrefix(.forwardSlash)
|
||||
else {
|
||||
defer {
|
||||
logger.error(
|
||||
"The request has issues.",
|
||||
metadata: .metadata(
|
||||
context: context,
|
||||
request: input,
|
||||
statusCode: .badRequest
|
||||
),
|
||||
source: .source
|
||||
)
|
||||
}
|
||||
|
||||
throw HTTPError(.badRequest)
|
||||
}
|
||||
|
||||
@ -70,11 +90,17 @@ struct DocCMiddleware<
|
||||
|
||||
// rule #5: Redirects URI resources with `/` to `/documentation`.
|
||||
if uriResource == .forwardSlash {
|
||||
return if uriPath.hasSuffix(.forwardSlash) {
|
||||
.redirect(to: String(format: .Format.Path.documentation, uriPath))
|
||||
let pathRedirect = if uriPath.hasSuffix(.forwardSlash) {
|
||||
String(format: .Format.Path.documentation, uriPath)
|
||||
} else {
|
||||
.redirect(to: String(format: .Format.Path.forwardSlash, uriPath))
|
||||
String(format: .Format.Path.forwardSlash, uriPath)
|
||||
}
|
||||
|
||||
return redirect(
|
||||
to: pathRedirect,
|
||||
input: input,
|
||||
context: context
|
||||
)
|
||||
}
|
||||
|
||||
for staticFile in StaticFile.allCases {
|
||||
@ -85,6 +111,7 @@ struct DocCMiddleware<
|
||||
return try await serveFile(
|
||||
String(format: .Format.Path.documentationJSON, nameArchive),
|
||||
at: pathArchive,
|
||||
input: input,
|
||||
context: context
|
||||
)
|
||||
} else {
|
||||
@ -93,6 +120,7 @@ struct DocCMiddleware<
|
||||
return try await serveFile(
|
||||
uriResource,
|
||||
at: pathArchive,
|
||||
input: input,
|
||||
context: context
|
||||
)
|
||||
}
|
||||
@ -106,6 +134,7 @@ struct DocCMiddleware<
|
||||
return try await serveFile(
|
||||
uriResource,
|
||||
at: pathArchive,
|
||||
input: input,
|
||||
context: context
|
||||
)
|
||||
}
|
||||
@ -118,15 +147,32 @@ struct DocCMiddleware<
|
||||
return try await serveFile(
|
||||
String(format: .Format.Path.index, indexPrefix.path, nameArchive),
|
||||
at: pathArchive,
|
||||
input: input,
|
||||
context: context
|
||||
)
|
||||
} else {
|
||||
// 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))
|
||||
return redirect(
|
||||
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)
|
||||
}
|
||||
@ -139,23 +185,79 @@ private extension DocCMiddleware {
|
||||
|
||||
// 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.
|
||||
/// - Parameters:
|
||||
/// - path: A relative path to a resource file.
|
||||
/// - folder: A folder accessible to the provider where to find resource files.
|
||||
/// - input: An input request.
|
||||
/// - 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,
|
||||
input: Request,
|
||||
context: Context
|
||||
) async throws -> Response {
|
||||
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)
|
||||
}
|
||||
|
||||
let body = try await assetProvider.loadFile(id: fileIdentifier, context: context)
|
||||
let body = try await assetProvider.loadFile(
|
||||
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(
|
||||
status: .ok,
|
||||
@ -165,3 +267,9 @@ private extension DocCMiddleware {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - String+Constants
|
||||
|
||||
private extension String {
|
||||
static let source = "DocCMiddleware"
|
||||
}
|
||||
|
@ -79,10 +79,14 @@ private extension AppBuilder {
|
||||
|
||||
router.addMiddleware {
|
||||
LogRequestsMiddleware(logger.logLevel)
|
||||
DocCMiddleware(archivesPath)
|
||||
DocCMiddleware(archivesPath, logger: logger)
|
||||
}
|
||||
|
||||
ArchiveController(archivesPath).register(to: router)
|
||||
ArchiveController(
|
||||
archivesPath,
|
||||
logger: logger
|
||||
)
|
||||
.register(to: router)
|
||||
|
||||
return router
|
||||
}
|
||||
|
4
Makefile
4
Makefile
@ -36,8 +36,8 @@ update: ## Updates the SPM package dependencies.
|
||||
build: ## Builds the project locally.
|
||||
@swift build -c release
|
||||
|
||||
run: ## Runs the project locally.
|
||||
@swift run -c release
|
||||
run: build ## Runs the project locally.
|
||||
@swift run doxy --archives-path "$(DOCC_ARCHIVES_FOLDER)" --log-level debug
|
||||
|
||||
tests: ## Runs all the test cases of the project.
|
||||
@swift test --enable-swift-testing
|
||||
|
11
README.md
11
README.md
@ -44,6 +44,17 @@ $ make install
|
||||
$ 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
|
||||
|
||||
To use this service, please do execute the following commands in the prompt of the **Terminal** command-line app in the local system:
|
||||
|
Loading…
x
Reference in New Issue
Block a user