feat: More cli client tests and documentation.
All checks were successful
CI / Run Tests (push) Successful in 4m57s

This commit is contained in:
2024-11-18 22:55:54 -05:00
parent 55ea88a29f
commit 99f39b91af
3 changed files with 114 additions and 22 deletions

View File

@@ -34,12 +34,23 @@ public struct CliClient {
/// Represents the parameters needed to create an `MQTTClient`. /// Represents the parameters needed to create an `MQTTClient`.
/// ///
///
public struct ClientRequest: Sendable { public struct ClientRequest: Sendable {
/// The environment variables used to create the client.
public let environment: EnvVars public let environment: EnvVars
/// The event loop group for the client.
public let eventLoopGroup: MultiThreadedEventLoopGroup public let eventLoopGroup: MultiThreadedEventLoopGroup
/// A logger to use with the client.
public let logger: Logger? public let logger: Logger?
/// Create a new client request.
///
/// - Parameters:
/// - environment: The environment variables to use.
/// - eventLoopGroup: The event loop group to use on the client.
/// - logger: An optional logger to use on the client.
public init( public init(
environment: EnvVars, environment: EnvVars,
eventLoopGroup: MultiThreadedEventLoopGroup, eventLoopGroup: MultiThreadedEventLoopGroup,
@@ -122,11 +133,10 @@ extension EnvVars {
@Dependency(\.environment) var environment @Dependency(\.environment) var environment
let defaultEnvVars = EnvVars() let defaultEnvVars = EnvVars()
let encoder = environment.jsonEncoder() let coders = environment.coders()
let decoder = environment.jsonDecoder()
let defaultEnvDict = (try? encoder.encode(defaultEnvVars)) let defaultEnvDict = (try? coders.encode(defaultEnvVars))
.flatMap { try? decoder.decode([String: String].self, from: $0) } .flatMap { try? coders.decode([String: String].self, from: $0) }
?? [:] ?? [:]
let dotEnvDict = try await environment.dotEnvDict(path: dotEnvFile) let dotEnvDict = try await environment.dotEnvDict(path: dotEnvFile)
@@ -135,7 +145,7 @@ extension EnvVars {
.merging(dotEnvDict, uniquingKeysWith: { $1 }) .merging(dotEnvDict, uniquingKeysWith: { $1 })
var envVars = (try? JSONSerialization.data(withJSONObject: envVarsDict)) var envVars = (try? JSONSerialization.data(withJSONObject: envVarsDict))
.flatMap { try? decoder.decode(EnvVars.self, from: $0) } .flatMap { try? coders.decode(EnvVars.self, from: $0) }
?? defaultEnvVars ?? defaultEnvVars
if let version { if let version {
@@ -162,7 +172,7 @@ public extension MQTTClient {
eventLoopGroupProvider: .shared(eventLoopGroup), eventLoopGroupProvider: .shared(eventLoopGroup),
logger: logger, logger: logger,
configuration: .init( configuration: .init(
version: .parseOrDefualt(string: envVars.version), version: .parseOrDefault(string: envVars.version),
disablePing: false, disablePing: false,
userName: envVars.userName, userName: envVars.userName,
password: envVars.password password: envVars.password
@@ -171,10 +181,11 @@ public extension MQTTClient {
} }
} }
extension MQTTClient.Version { @_spi(Internal)
public extension MQTTClient.Version {
static let `default` = Self.v3_1_1 static let `default` = Self.v3_1_1
static func parseOrDefualt(string: String?) -> Self { static func parseOrDefault(string: String?) -> Self {
guard let string, let value = Self(string: string) else { guard let string, let value = Self(string: string) else {
return .default return .default
} }

View File

@@ -24,11 +24,7 @@ public extension DependencyValues {
@DependencyClient @DependencyClient
public struct EnvironmentDependency: Sendable { public struct EnvironmentDependency: Sendable {
/// A json decoder to use to decode files and environment variables. public var coders: @Sendable () -> any Coderable = { JSONCoders() }
public var jsonDecoder: @Sendable () -> JSONDecoder = { JSONDecoder() }
/// A json encoder to use to encode files and environment variables.
public var jsonEncoder: @Sendable () -> JSONEncoder = { JSONEncoder() }
/// Load the variables based on the request. /// Load the variables based on the request.
public var load: @Sendable (FileType) async throws -> [String: String] = { _ in [:] } public var load: @Sendable (FileType) async throws -> [String: String] = { _ in [:] }
@@ -61,6 +57,8 @@ public struct EnvironmentDependency: Sendable {
} }
} }
struct DecodeError: Error {}
@_spi(Internal) @_spi(Internal)
extension EnvironmentDependency: DependencyKey { extension EnvironmentDependency: DependencyKey {
@@ -71,8 +69,7 @@ extension EnvironmentDependency: DependencyKey {
encoder: JSONEncoder = .init() encoder: JSONEncoder = .init()
) -> Self { ) -> Self {
Self( Self(
jsonDecoder: { decoder }, coders: { JSONCoders(decoder: decoder, encoder: encoder) },
jsonEncoder: { encoder },
load: { file in load: { file in
switch file { switch file {
case let .dotEnv(path: path): case let .dotEnv(path: path):
@@ -95,6 +92,39 @@ extension EnvironmentDependency: DependencyKey {
public static let liveValue: EnvironmentDependency = .live() public static let liveValue: EnvironmentDependency = .live()
} }
/// A type that encode and decode values.
///
/// This is really just here to override tests with coders that will throw an error,
/// instead of encoding or decoding data.
///
@_spi(Internal)
public protocol Coderable {
func encode<T: Encodable>(_ instance: T) throws -> Data
func decode<T: Decodable>(_ type: T.Type, from data: Data) throws -> T
}
struct JSONCoders: Coderable {
let decoder: JSONDecoder
let encoder: JSONEncoder
init(
decoder: JSONDecoder = .init(),
encoder: JSONEncoder = .init()
) {
self.decoder = decoder
self.encoder = encoder
}
func encode<T>(_ instance: T) throws -> Data where T: Encodable {
try encoder.encode(instance)
}
func decode<T>(_ type: T.Type, from data: Data) throws -> T where T: Decodable {
try decoder.decode(T.self, from: data)
}
}
private func url(for path: String) -> URL { private func url(for path: String) -> URL {
#if os(Linux) #if os(Linux)
return URL(fileURLWithPath: path) return URL(fileURLWithPath: path)

View File

@@ -32,6 +32,8 @@ final class CliClientTests: XCTestCase {
XCTAssertEqual(cliClient.parseMqttClientVersion(string), version) XCTAssertEqual(cliClient.parseMqttClientVersion(string), version)
} }
} }
XCTAssertEqual(MQTTClient.Version.parseOrDefault(string: nil), .v3_1_1)
} }
func testLogLevelFromEnvironment() { func testLogLevelFromEnvironment() {
@@ -98,6 +100,26 @@ final class CliClientTests: XCTestCase {
} }
} }
func testMakeEnvVarsWithFailingDecoder() async throws {
try await withDependencies {
$0.environment.coders = { ThrowingDecoder() }
} operation: {
@Dependency(\.cliClient) var cliClient
let envVars = try await cliClient.makeEnvVars(.init())
XCTAssertEqual(envVars, EnvVars())
}
}
func testMakeEnvVarsWithFailingEncoder() async throws {
try await withDependencies {
$0.environment.coders = { ThrowingEncoder() }
} operation: {
@Dependency(\.cliClient) var cliClient
let envVars = try await cliClient.makeEnvVars(.init())
XCTAssertEqual(envVars, EnvVars())
}
}
func testFileType() { func testFileType() {
let arguments = [ let arguments = [
(EnvironmentDependency.FileType.dotEnv(path: "test.env"), "test.env"), (EnvironmentDependency.FileType.dotEnv(path: "test.env"), "test.env"),
@@ -139,12 +161,16 @@ final class CliClientTests: XCTestCase {
// - MARK: Helper // - MARK: Helper
private func cleanFilePath(_ path: String) -> String { private func cleanFilePath(_ path: String) -> String {
let split = path.split(separator: ".") #if os(Linux)
let fileName = split.first! return "Tests/CliClientTests/\(path)"
let ext = split.last! #else
let url = Bundle.module.url(forResource: String(fileName), withExtension: String(ext))!.absoluteString let split = path.split(separator: ".")
let cleaned = url.split(separator: "file://").last! let fileName = split.first!
return String(cleaned) let ext = split.last!
let url = Bundle.module.url(forResource: String(fileName), withExtension: String(ext))!.absoluteString
let cleaned = url.split(separator: "file://").last!
return String(cleaned)
#endif
} }
extension EnvVars { extension EnvVars {
@@ -159,3 +185,28 @@ extension EnvVars {
version: "5.0" version: "5.0"
) )
} }
struct ThrowingDecoder: Coderable {
func encode<T>(_ instance: T) throws -> Data where T: Encodable {
try JSONEncoder().encode(instance)
}
func decode<T>(_ type: T.Type, from data: Data) throws -> T where T: Decodable {
throw DecodeError()
}
}
struct ThrowingEncoder: Coderable {
func encode<T>(_ instance: T) throws -> Data where T: Encodable {
throw EncodeError()
}
func decode<T>(_ type: T.Type, from data: Data) throws -> T where T: Decodable {
try JSONDecoder().decode(T.self, from: data)
}
}
struct DecodeError: Error {}
struct EncodeError: Error {}