From 99f39b91af279b9912631f0db35c0a4743512c27 Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Mon, 18 Nov 2024 22:55:54 -0500 Subject: [PATCH] feat: More cli client tests and documentation. --- Sources/CliClient/CliClient.swift | 29 ++++++--- Sources/CliClient/EnvironmentDependency.swift | 44 ++++++++++--- Tests/CliClientTests/CliClientTests.swift | 63 +++++++++++++++++-- 3 files changed, 114 insertions(+), 22 deletions(-) diff --git a/Sources/CliClient/CliClient.swift b/Sources/CliClient/CliClient.swift index fd9f4cf..260ab4b 100644 --- a/Sources/CliClient/CliClient.swift +++ b/Sources/CliClient/CliClient.swift @@ -34,12 +34,23 @@ public struct CliClient { /// Represents the parameters needed to create an `MQTTClient`. /// - /// public struct ClientRequest: Sendable { + + /// The environment variables used to create the client. public let environment: EnvVars + + /// The event loop group for the client. public let eventLoopGroup: MultiThreadedEventLoopGroup + + /// A logger to use with the client. 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( environment: EnvVars, eventLoopGroup: MultiThreadedEventLoopGroup, @@ -122,11 +133,10 @@ extension EnvVars { @Dependency(\.environment) var environment let defaultEnvVars = EnvVars() - let encoder = environment.jsonEncoder() - let decoder = environment.jsonDecoder() + let coders = environment.coders() - let defaultEnvDict = (try? encoder.encode(defaultEnvVars)) - .flatMap { try? decoder.decode([String: String].self, from: $0) } + let defaultEnvDict = (try? coders.encode(defaultEnvVars)) + .flatMap { try? coders.decode([String: String].self, from: $0) } ?? [:] let dotEnvDict = try await environment.dotEnvDict(path: dotEnvFile) @@ -135,7 +145,7 @@ extension EnvVars { .merging(dotEnvDict, uniquingKeysWith: { $1 }) var envVars = (try? JSONSerialization.data(withJSONObject: envVarsDict)) - .flatMap { try? decoder.decode(EnvVars.self, from: $0) } + .flatMap { try? coders.decode(EnvVars.self, from: $0) } ?? defaultEnvVars if let version { @@ -162,7 +172,7 @@ public extension MQTTClient { eventLoopGroupProvider: .shared(eventLoopGroup), logger: logger, configuration: .init( - version: .parseOrDefualt(string: envVars.version), + version: .parseOrDefault(string: envVars.version), disablePing: false, userName: envVars.userName, 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 func parseOrDefualt(string: String?) -> Self { + static func parseOrDefault(string: String?) -> Self { guard let string, let value = Self(string: string) else { return .default } diff --git a/Sources/CliClient/EnvironmentDependency.swift b/Sources/CliClient/EnvironmentDependency.swift index e985668..99d9264 100644 --- a/Sources/CliClient/EnvironmentDependency.swift +++ b/Sources/CliClient/EnvironmentDependency.swift @@ -24,11 +24,7 @@ public extension DependencyValues { @DependencyClient public struct EnvironmentDependency: Sendable { - /// A json decoder to use to decode files and environment variables. - public var jsonDecoder: @Sendable () -> JSONDecoder = { JSONDecoder() } - - /// A json encoder to use to encode files and environment variables. - public var jsonEncoder: @Sendable () -> JSONEncoder = { JSONEncoder() } + public var coders: @Sendable () -> any Coderable = { JSONCoders() } /// Load the variables based on the request. public var load: @Sendable (FileType) async throws -> [String: String] = { _ in [:] } @@ -61,6 +57,8 @@ public struct EnvironmentDependency: Sendable { } } +struct DecodeError: Error {} + @_spi(Internal) extension EnvironmentDependency: DependencyKey { @@ -71,8 +69,7 @@ extension EnvironmentDependency: DependencyKey { encoder: JSONEncoder = .init() ) -> Self { Self( - jsonDecoder: { decoder }, - jsonEncoder: { encoder }, + coders: { JSONCoders(decoder: decoder, encoder: encoder) }, load: { file in switch file { case let .dotEnv(path: path): @@ -95,6 +92,39 @@ extension EnvironmentDependency: DependencyKey { 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(_ instance: T) throws -> Data + func decode(_ 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(_ instance: T) throws -> Data where T: Encodable { + try encoder.encode(instance) + } + + func decode(_ 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 { #if os(Linux) return URL(fileURLWithPath: path) diff --git a/Tests/CliClientTests/CliClientTests.swift b/Tests/CliClientTests/CliClientTests.swift index 0af6b31..4a544e2 100644 --- a/Tests/CliClientTests/CliClientTests.swift +++ b/Tests/CliClientTests/CliClientTests.swift @@ -32,6 +32,8 @@ final class CliClientTests: XCTestCase { XCTAssertEqual(cliClient.parseMqttClientVersion(string), version) } } + + XCTAssertEqual(MQTTClient.Version.parseOrDefault(string: nil), .v3_1_1) } 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() { let arguments = [ (EnvironmentDependency.FileType.dotEnv(path: "test.env"), "test.env"), @@ -139,12 +161,16 @@ final class CliClientTests: XCTestCase { // - MARK: Helper private func cleanFilePath(_ path: String) -> String { - let split = path.split(separator: ".") - let fileName = split.first! - 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) + #if os(Linux) + return "Tests/CliClientTests/\(path)" + #else + let split = path.split(separator: ".") + let fileName = split.first! + 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 { @@ -159,3 +185,28 @@ extension EnvVars { version: "5.0" ) } + +struct ThrowingDecoder: Coderable { + + func encode(_ instance: T) throws -> Data where T: Encodable { + try JSONEncoder().encode(instance) + } + + func decode(_ type: T.Type, from data: Data) throws -> T where T: Decodable { + throw DecodeError() + } +} + +struct ThrowingEncoder: Coderable { + + func encode(_ instance: T) throws -> Data where T: Encodable { + throw EncodeError() + } + + func decode(_ type: T.Type, from data: Data) throws -> T where T: Decodable { + try JSONDecoder().decode(T.self, from: data) + } +} + +struct DecodeError: Error {} +struct EncodeError: Error {}