Added (first version of) sample Hummingbird app. (#4)
This PR contains the work done to: * Implemented a basic `Hummingbird` application in which to integrate the `HummingbirdDocC` library. * Added the *ArgumentParser* package dependency to the `Package.swift` file; * Added a new *sample* target to the `Package.swift` file; * Added library and documentation tasks to the `Makefile` file. Reviewed-on: #4 Co-authored-by: Javier Cicchelli <javier@rock-n-code.com> Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
This commit was merged in pull request #4.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the Hummingbird DocC Middleware open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
|
||||
// Licensed under the EUPL 1.2 or later.
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
import protocol Logging.LogHandler
|
||||
|
||||
import struct Logging.Logger
|
||||
|
||||
extension Logger {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// Generates a logger instance that is ready to use in test cases.
|
||||
/// - Parameters:
|
||||
/// - level: A logger level, if any.
|
||||
/// - handler: A custom log handler, if any.
|
||||
/// - Returns: A generated logger instance ready to use in test cases.
|
||||
static func test(
|
||||
level: Logger.Level? = nil,
|
||||
handler: (any LogHandler)? = nil
|
||||
) -> Self {
|
||||
var logger: Logger = if let handler {
|
||||
.init(label: .loggerLabel) { _ in handler }
|
||||
} else {
|
||||
.init(label: .loggerLabel)
|
||||
}
|
||||
|
||||
logger.logLevel = if let level {
|
||||
level
|
||||
} else {
|
||||
try! #require(Logger.Level.allCases.randomElement())
|
||||
}
|
||||
|
||||
logger[metadataKey: "hb.request.id"] = "\(UUID().uuidString)"
|
||||
|
||||
return logger
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
private extension String {
|
||||
/// A label to assign to a test logger instance.
|
||||
static let loggerLabel = "test.hummingbird-docc.logger"
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the Hummingbird DocC Middleware open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
|
||||
// Licensed under the EUPL 1.2 or later.
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
import Testing
|
||||
|
||||
import struct Logging.Logger
|
||||
|
||||
extension Logger.Level {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// Extracts a random logging level value out of an inclusive subset of logging levels, arranged by severity.
|
||||
/// - Parameter level: A representation of a logging level that defines a subset of values to choose from, if any.
|
||||
/// - Returns: A randomized logging value.
|
||||
/// - Throws: An error thrown in case an issue is encountered when deciding for a random value.
|
||||
static func random(upTo level: Self? = nil) throws -> Self {
|
||||
guard let level else {
|
||||
return try #require(Self.allCases.randomElement())
|
||||
}
|
||||
|
||||
let levels: [Self] = switch level {
|
||||
case .trace: [.trace]
|
||||
case .debug: [.debug, .trace]
|
||||
case .info: [.debug, .info, .trace]
|
||||
case .notice: [.debug, .info, .notice, .trace]
|
||||
case .warning: [.debug, .info, .notice, .trace, .warning]
|
||||
case .error: [.debug, .error, .info, .notice, .trace, .warning]
|
||||
case .critical: Self.allCases
|
||||
}
|
||||
|
||||
return try #require(levels.randomElement())
|
||||
}
|
||||
|
||||
/// /// Extracts a random logging level value out of an exclusive subset of logging levels, arranged by severity.
|
||||
/// - Parameter level: A representation of a logging level that defines a subset of values to choose from.
|
||||
/// - Returns: A randomized logging value.
|
||||
/// - Throws: An error thrown in case an issue is encountered when deciding for a random value.
|
||||
static func random(fromExclusive level: Self) throws -> Self {
|
||||
let levels: [Self] = switch level {
|
||||
case .trace: [.critical, .debug, .error, .info, .notice, .warning]
|
||||
case .debug: [.critical, .error, .info, .notice, .warning]
|
||||
case .info: [.critical, .error, .notice, .warning]
|
||||
case .notice: [.critical, .error, .warning]
|
||||
case .warning: [.critical, .error]
|
||||
case .error: [.critical]
|
||||
case .critical: []
|
||||
}
|
||||
|
||||
return try #require(levels.randomElement())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the Hummingbird DocC Middleware open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
|
||||
// Licensed under the EUPL 1.2 or later.
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
import struct Hummingbird.HTTPRequest
|
||||
import struct Hummingbird.Request
|
||||
import struct Hummingbird.RequestBody
|
||||
|
||||
extension Request {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// Generates a request that is ready to use in test case.
|
||||
/// - Parameters:
|
||||
/// - method: A HTTP method.
|
||||
/// - path: A URI path, if any.
|
||||
/// - Returns: A generated request instance to use in test cases.
|
||||
static func test(
|
||||
method: HTTPRequest.Method,
|
||||
path: String? = nil
|
||||
) -> Self {
|
||||
.init(
|
||||
head: .init(
|
||||
method: method,
|
||||
scheme: nil,
|
||||
authority: nil,
|
||||
path: path
|
||||
),
|
||||
body: .init(buffer: .init())
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the Hummingbird DocC Middleware open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
|
||||
// Licensed under the EUPL 1.2 or later.
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
extension String {
|
||||
|
||||
// MARK: Constants
|
||||
|
||||
/// A namespace that defines sample values.
|
||||
enum Sample {
|
||||
/// A URI path to use as a documentation root sample.
|
||||
static let uriDocument = uriRoot + "/SomeDocument"
|
||||
/// A URI path to use as a file sample.
|
||||
static let uriFile = uriFolder + uriResource
|
||||
/// A URI path to use as a folder sample.
|
||||
static let uriFolder = "/some/folder/path"
|
||||
/// A URI path to use as a redirection sample.
|
||||
static let uriRedirection = "/some/redirect/path"
|
||||
/// A URI path to use as a resource sample.
|
||||
static let uriResource = "/some/path/to/resource"
|
||||
/// A URI path to use as a root sample.
|
||||
static let uriRoot = "/some/root/path"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the Hummingbird DocC Middleware open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
|
||||
// Licensed under the EUPL 1.2 or later.
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
import Testing
|
||||
|
||||
extension Tag {
|
||||
|
||||
// MARK: Constants
|
||||
|
||||
/// Tag that indicate a test case for an enumeration type.
|
||||
@Tag static var enumeration: Self
|
||||
/// Tag that indicate a test case for an extended type.
|
||||
@Tag static var `extension`: Self
|
||||
/// Tag that indicate a test case for a middleware type.
|
||||
@Tag static var middleware: Self
|
||||
/// Tag that indicate a test case for a model type.
|
||||
@Tag static var model: Self
|
||||
/// Tag that indicate a test case for a use case type.
|
||||
@Tag static var useCase: Self
|
||||
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the Hummingbird DocC Middleware open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
|
||||
// Licensed under the EUPL 1.2 or later.
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
import protocol Hummingbird.FileProvider
|
||||
import protocol Hummingbird.RequestContext
|
||||
|
||||
import struct Foundation.Data
|
||||
import struct Foundation.UUID
|
||||
import struct Hummingbird.ResponseBody
|
||||
|
||||
/// A mock that conforms to the `FileProvider` protocol.
|
||||
struct FileProviderMock {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// A type that identifies a sample file.
|
||||
private let fileIdentifier: UUID?
|
||||
|
||||
/// A flag that indicates whether a file should be loaded or not.
|
||||
private let shouldLoadFile: Bool
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Initializes this mock.
|
||||
/// - Parameters:
|
||||
/// - fileIdentifier: A type that identifies a sample file, if any.
|
||||
/// - shouldLoadFile: A flag that indicates whether a file should be loaded or not.
|
||||
init(
|
||||
fileIdentifier: UUID? = nil,
|
||||
shouldLoadFile: Bool = true
|
||||
) {
|
||||
self.fileIdentifier = fileIdentifier
|
||||
self.shouldLoadFile = shouldLoadFile
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - FileProvider
|
||||
|
||||
extension FileProviderMock: FileProvider {
|
||||
|
||||
// MARK: Type aliases
|
||||
|
||||
typealias FileAttributes = String
|
||||
typealias FileIdentifier = String
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
func getFileIdentifier(_ path: String) -> String? {
|
||||
fileIdentifier?.uuidString
|
||||
}
|
||||
|
||||
func getAttributes(id: String) async throws -> String? {
|
||||
nil
|
||||
}
|
||||
|
||||
func loadFile(
|
||||
id: String,
|
||||
context: some RequestContext
|
||||
) async throws -> ResponseBody {
|
||||
guard shouldLoadFile else {
|
||||
throw FileProviderMockError.fileNotLoaded
|
||||
}
|
||||
|
||||
guard let content = fileIdentifier?.uuidString else {
|
||||
return .init()
|
||||
}
|
||||
|
||||
return .init(byteBuffer: .init(
|
||||
data: .init(content.utf8)
|
||||
))
|
||||
}
|
||||
|
||||
func loadFile(
|
||||
id: String,
|
||||
range: ClosedRange<Int>,
|
||||
context: some RequestContext
|
||||
) async throws -> ResponseBody {
|
||||
try await loadFile(
|
||||
id: id,
|
||||
context: context
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - FileProviderMockError
|
||||
|
||||
/// An error type that can only be thrown by the ``FileProviderMock`` mock.
|
||||
enum FileProviderMockError: Error {
|
||||
/// An error encountered while mocking the loading of a file.
|
||||
case fileNotLoaded
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the Hummingbird DocC Middleware open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
|
||||
// Licensed under the EUPL 1.2 or later.
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
import Foundation
|
||||
|
||||
import protocol Logging.LogHandler
|
||||
|
||||
import struct Logging.Logger
|
||||
|
||||
/// A mock that conforms to the `LogHandler` protocol.
|
||||
struct LogHandlerMock {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// A representation of the logging level assigned to this mock.
|
||||
private var _logLevel: Logger.Level = .debug
|
||||
|
||||
/// A dictionary that contains all the metadata assigned to this mock.
|
||||
private var _metadata: Logger.Metadata = [:]
|
||||
|
||||
/// A logging event recorder attached to this mock.
|
||||
private let recorder: LogRecorder = .init()
|
||||
|
||||
// MARK: Computed
|
||||
|
||||
/// A list of all the logged events that are being persisted in the recorder.
|
||||
var entries: [LogEntry] { recorder.entries }
|
||||
|
||||
}
|
||||
|
||||
// MARK: - LogEntry
|
||||
|
||||
/// A type that contains the information logged in a logging event.
|
||||
struct LogEntry: Equatable {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// A representation of the level attached to a logged event.
|
||||
let level: Logger.Level
|
||||
|
||||
/// A metadata dictionary that contains additional information attached to a logged event.
|
||||
let metadata: Logger.Metadata?
|
||||
|
||||
/// A message attached to a logged event.
|
||||
let message: Logger.Message
|
||||
|
||||
/// A source from where a logged event was triggered.
|
||||
let source: String
|
||||
|
||||
}
|
||||
|
||||
// MARK: - LogRecorder
|
||||
|
||||
extension LogHandlerMock {
|
||||
/// A class that records all the events logged by the ``LogHandlerMock`` mock handler.
|
||||
///
|
||||
/// This class conforms to the `Sendable` protocol by using the `@unchecked` modifier because a `NSLock`type is used to handle the access to the logged events in a thread-safe way.
|
||||
final class LogRecorder: @unchecked Sendable {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// A list of all the logged events persisted in a thread-safe way.
|
||||
private(set) var _entries: [LogEntry] = []
|
||||
|
||||
/// A type that coordinates the access to the persisted logged events in a thread-safe way.
|
||||
private let lock: NSLock = .init()
|
||||
|
||||
// MARK: Computed
|
||||
|
||||
/// A list of all the logged events.
|
||||
var entries: [LogEntry] {
|
||||
lock.withLock { _entries }
|
||||
}
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// Records data related to a logged event.
|
||||
/// - Parameters:
|
||||
/// - level: A representation of the level attached to a logged event.
|
||||
/// - metadata: A metadata dictionary that contains additional information attached to a logged event.
|
||||
/// - message: A message attached to a logged event.
|
||||
/// - source: A source from where a logged event was triggered.
|
||||
func record(
|
||||
level: Logger.Level,
|
||||
metadata: Logger.Metadata?,
|
||||
message: Logger.Message,
|
||||
source: String
|
||||
) {
|
||||
lock.withLock {
|
||||
_entries += [.init(
|
||||
level: level,
|
||||
metadata: metadata,
|
||||
message: message,
|
||||
source: source
|
||||
)]
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - LogHandler
|
||||
|
||||
extension LogHandlerMock: LogHandler {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
var metadata: Logger.Metadata {
|
||||
get { _metadata }
|
||||
set(newValue) { _metadata = newValue }
|
||||
}
|
||||
|
||||
var logLevel: Logger.Level {
|
||||
get { _logLevel }
|
||||
set(newValue) { _logLevel = newValue }
|
||||
}
|
||||
|
||||
// MARK: Subscripts
|
||||
|
||||
subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? {
|
||||
get { _metadata[metadataKey] }
|
||||
set(newValue) { _metadata[metadataKey] = newValue }
|
||||
}
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
func log(
|
||||
level: Logger.Level,
|
||||
message: Logger.Message,
|
||||
metadata: Logger.Metadata?,
|
||||
source: String,
|
||||
file: String,
|
||||
function: String,
|
||||
line: UInt
|
||||
) {
|
||||
recorder.record(
|
||||
level: level,
|
||||
metadata: metadata,
|
||||
message: message,
|
||||
source: source
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the Hummingbird DocC Middleware open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
|
||||
// Licensed under the EUPL 1.2 or later.
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
import class NIOEmbedded.NIOAsyncTestingChannel
|
||||
|
||||
import protocol Hummingbird.RequestContext
|
||||
|
||||
import struct Hummingbird.ApplicationRequestContextSource
|
||||
import struct Hummingbird.CoreRequestContextStorage
|
||||
import struct Logging.Logger
|
||||
|
||||
/// A mock that conforms to the `RequestContext` protocol.
|
||||
struct RequestContextMock {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
var coreContext: CoreRequestContextStorage
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Initializes this mock.
|
||||
/// - Parameter logger: A type that interacts with the logging system.
|
||||
init(logger: Logger) {
|
||||
self.coreContext = .init(source: ApplicationRequestContextSource(
|
||||
channel: NIOAsyncTestingChannel(),
|
||||
logger: logger
|
||||
))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - RequestContext
|
||||
|
||||
extension RequestContextMock: RequestContext {
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
init(source: ApplicationRequestContextSource) {
|
||||
self.coreContext = .init(source: source)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the Hummingbird DocC Middleware open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
|
||||
// Licensed under the EUPL 1.2 or later.
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
/// A namespace assigned for test arguments
|
||||
enum Input {}
|
||||
@@ -0,0 +1,14 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the Hummingbird DocC Middleware open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
|
||||
// Licensed under the EUPL 1.2 or later.
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
/// A namespace assigned for test arguments that would be expected outputs coming from results of test cases.
|
||||
enum Output {}
|
||||
@@ -0,0 +1,53 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the Hummingbird DocC Middleware open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
|
||||
// Licensed under the EUPL 1.2 or later.
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
import Hummingbird
|
||||
|
||||
/// A stub that conforms to the `FileProvider` protocol.
|
||||
struct FileProviderStub {}
|
||||
|
||||
// MARK: - FileProvider
|
||||
|
||||
extension FileProviderStub: FileProvider {
|
||||
|
||||
// MARK: Type aliases
|
||||
|
||||
typealias FileAttributes = String
|
||||
typealias FileIdentifier = String
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
func getFileIdentifier(_ path: String) -> String? {
|
||||
nil
|
||||
}
|
||||
|
||||
func getAttributes(id: String) async throws -> String? {
|
||||
nil
|
||||
}
|
||||
|
||||
func loadFile(
|
||||
id: String,
|
||||
context: some RequestContext
|
||||
) async throws -> ResponseBody {
|
||||
.init()
|
||||
}
|
||||
|
||||
func loadFile(
|
||||
id: String,
|
||||
range: ClosedRange<Int>,
|
||||
context: some RequestContext
|
||||
) async throws -> ResponseBody {
|
||||
.init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user