From c82f4ec404965ff367c9ae1b08cd720a1ec78245 Mon Sep 17 00:00:00 2001 From: James J Kalafus Date: Wed, 14 Feb 2024 22:48:17 -0500 Subject: [PATCH] Encodable extensions for use in testing stubs. --- Sources/OpenAI/Public/Errors/APIError.swift | 2 +- .../Models/AudioTranscriptionResult.swift | 4 + .../Models/AudioTranslationResult.swift | 4 + Sources/OpenAI/Public/Models/ChatResult.swift | 6 +- .../Public/Models/ChatStreamResult.swift | 6 +- .../Public/Models/CompletionsResult.swift | 21 +- .../OpenAI/Public/Models/EditsResult.swift | 20 +- .../Public/Models/EmbeddingsResult.swift | 14 +- .../OpenAI/Public/Models/ImagesResult.swift | 14 +- .../Public/Models/Models/ModelResult.swift | 2 +- .../Public/Models/Models/ModelsResult.swift | 5 + .../Public/Models/ModerationsResult.swift | 12 +- .../Mocks/EncodableExtensions.swift | 244 ++++++++++++++++++ Tests/OpenAITests/Mocks/URLSessionMock.swift | 4 +- Tests/OpenAITests/OpenAITests.swift | 2 +- Tests/OpenAITests/OpenAITestsCombine.swift | 2 +- 16 files changed, 334 insertions(+), 28 deletions(-) create mode 100644 Tests/OpenAITests/Mocks/EncodableExtensions.swift diff --git a/Sources/OpenAI/Public/Errors/APIError.swift b/Sources/OpenAI/Public/Errors/APIError.swift index 2856fb83..b7e2f131 100644 --- a/Sources/OpenAI/Public/Errors/APIError.swift +++ b/Sources/OpenAI/Public/Errors/APIError.swift @@ -24,7 +24,7 @@ public struct APIError: Error, Decodable, Equatable { self.code = code } - enum CodingKeys: CodingKey { + public enum CodingKeys: CodingKey { case message case type case param diff --git a/Sources/OpenAI/Public/Models/AudioTranscriptionResult.swift b/Sources/OpenAI/Public/Models/AudioTranscriptionResult.swift index 3b1e2c36..d4fbb2e2 100644 --- a/Sources/OpenAI/Public/Models/AudioTranscriptionResult.swift +++ b/Sources/OpenAI/Public/Models/AudioTranscriptionResult.swift @@ -10,4 +10,8 @@ import Foundation public struct AudioTranscriptionResult: Decodable, Equatable { public let text: String + + public enum CodingKeys: CodingKey { + case text + } } diff --git a/Sources/OpenAI/Public/Models/AudioTranslationResult.swift b/Sources/OpenAI/Public/Models/AudioTranslationResult.swift index 954d650f..f82857ad 100644 --- a/Sources/OpenAI/Public/Models/AudioTranslationResult.swift +++ b/Sources/OpenAI/Public/Models/AudioTranslationResult.swift @@ -10,4 +10,8 @@ import Foundation public struct AudioTranslationResult: Decodable, Equatable { public let text: String + + public enum CodingKeys: CodingKey { + case text + } } diff --git a/Sources/OpenAI/Public/Models/ChatResult.swift b/Sources/OpenAI/Public/Models/ChatResult.swift index 46885a11..4ee24785 100644 --- a/Sources/OpenAI/Public/Models/ChatResult.swift +++ b/Sources/OpenAI/Public/Models/ChatResult.swift @@ -17,7 +17,7 @@ public struct ChatResult: Decodable, Equatable { /// Exists only if it is a complete message. public let finishReason: String? - enum CodingKeys: String, CodingKey { + public enum CodingKeys: String, CodingKey { case index case message case finishReason = "finish_reason" @@ -29,7 +29,7 @@ public struct ChatResult: Decodable, Equatable { public let completionTokens: Int public let totalTokens: Int - enum CodingKeys: String, CodingKey { + public enum CodingKeys: String, CodingKey { case promptTokens = "prompt_tokens" case completionTokens = "completion_tokens" case totalTokens = "total_tokens" @@ -43,7 +43,7 @@ public struct ChatResult: Decodable, Equatable { public let choices: [Choice] public let usage: Usage? - enum CodingKeys: String, CodingKey { + public enum CodingKeys: String, CodingKey { case id case object case created diff --git a/Sources/OpenAI/Public/Models/ChatStreamResult.swift b/Sources/OpenAI/Public/Models/ChatStreamResult.swift index 201cc0f3..2a886ab0 100644 --- a/Sources/OpenAI/Public/Models/ChatStreamResult.swift +++ b/Sources/OpenAI/Public/Models/ChatStreamResult.swift @@ -17,7 +17,7 @@ public struct ChatStreamResult: Decodable, Equatable { public let name: String? public let functionCall: ChatFunctionCall? - enum CodingKeys: String, CodingKey { + public enum CodingKeys: String, CodingKey { case role case content case name @@ -29,7 +29,7 @@ public struct ChatStreamResult: Decodable, Equatable { public let delta: Delta public let finishReason: String? - enum CodingKeys: String, CodingKey { + public enum CodingKeys: String, CodingKey { case index case delta case finishReason = "finish_reason" @@ -42,7 +42,7 @@ public struct ChatStreamResult: Decodable, Equatable { public let model: Model public let choices: [Choice] - enum CodingKeys: String, CodingKey { + public enum CodingKeys: String, CodingKey { case id case object case created diff --git a/Sources/OpenAI/Public/Models/CompletionsResult.swift b/Sources/OpenAI/Public/Models/CompletionsResult.swift index fa0c4dbd..64f98ad5 100644 --- a/Sources/OpenAI/Public/Models/CompletionsResult.swift +++ b/Sources/OpenAI/Public/Models/CompletionsResult.swift @@ -8,25 +8,25 @@ import Foundation public struct CompletionsResult: Decodable, Equatable { - + public struct Usage: Decodable, Equatable { public let promptTokens: Int public let completionTokens: Int public let totalTokens: Int - - enum CodingKeys: String, CodingKey { + + public enum CodingKeys: String, CodingKey { case promptTokens = "prompt_tokens" case completionTokens = "completion_tokens" case totalTokens = "total_tokens" } } - + public struct Choice: Decodable, Equatable { public let text: String public let index: Int public let finishReason: String? - - enum CodingKeys: String, CodingKey { + + public enum CodingKeys: String, CodingKey { case text case index case finishReason = "finish_reason" @@ -39,4 +39,13 @@ public struct CompletionsResult: Decodable, Equatable { public let model: Model public let choices: [Choice] public let usage: Usage? + + public enum CodingKeys: CodingKey { + case id + case object + case created + case model + case choices + case usage + } } diff --git a/Sources/OpenAI/Public/Models/EditsResult.swift b/Sources/OpenAI/Public/Models/EditsResult.swift index 04a6b0e2..90ae6114 100644 --- a/Sources/OpenAI/Public/Models/EditsResult.swift +++ b/Sources/OpenAI/Public/Models/EditsResult.swift @@ -8,26 +8,38 @@ import Foundation public struct EditsResult: Decodable, Equatable { - + public struct Choice: Decodable, Equatable { public let text: String public let index: Int + + public enum CodingKeys: CodingKey { + case text + case index + } } public struct Usage: Decodable, Equatable { public let promptTokens: Int public let completionTokens: Int public let totalTokens: Int - - enum CodingKeys: String, CodingKey { + + public enum CodingKeys: String, CodingKey { case promptTokens = "prompt_tokens" case completionTokens = "completion_tokens" case totalTokens = "total_tokens" } } - + public let object: String public let created: TimeInterval public let choices: [Choice] public let usage: Usage + + public enum CodingKeys: CodingKey { + case object + case created + case choices + case usage + } } diff --git a/Sources/OpenAI/Public/Models/EmbeddingsResult.swift b/Sources/OpenAI/Public/Models/EmbeddingsResult.swift index 5e0c35c2..4fa89dec 100644 --- a/Sources/OpenAI/Public/Models/EmbeddingsResult.swift +++ b/Sources/OpenAI/Public/Models/EmbeddingsResult.swift @@ -13,13 +13,19 @@ public struct EmbeddingsResult: Decodable, Equatable { public let object: String public let embedding: [Double] public let index: Int + + public enum CodingKeys: CodingKey { + case object + case embedding + case index + } } public struct Usage: Decodable, Equatable { public let promptTokens: Int public let totalTokens: Int - enum CodingKeys: String, CodingKey { + public enum CodingKeys: String, CodingKey { case promptTokens = "prompt_tokens" case totalTokens = "total_tokens" } @@ -28,4 +34,10 @@ public struct EmbeddingsResult: Decodable, Equatable { public let data: [Embedding] public let model: Model public let usage: Usage + + public enum CodingKeys: CodingKey { + case data + case model + case usage + } } diff --git a/Sources/OpenAI/Public/Models/ImagesResult.swift b/Sources/OpenAI/Public/Models/ImagesResult.swift index e2b35e9f..9b99b661 100644 --- a/Sources/OpenAI/Public/Models/ImagesResult.swift +++ b/Sources/OpenAI/Public/Models/ImagesResult.swift @@ -8,14 +8,24 @@ import Foundation public struct ImagesResult: Decodable, Equatable { - + public struct URLResult: Decodable, Equatable { public let url: String? public let b64_json: String? + + public enum CodingKeys: CodingKey { + case url + case b64_json + } } - + public let created: TimeInterval public let data: [URLResult] + + public enum CodingKeys: CodingKey { + case created + case data + } } extension ImagesResult.URLResult: Hashable { } diff --git a/Sources/OpenAI/Public/Models/Models/ModelResult.swift b/Sources/OpenAI/Public/Models/Models/ModelResult.swift index 97fccadc..4b7d11cb 100644 --- a/Sources/OpenAI/Public/Models/Models/ModelResult.swift +++ b/Sources/OpenAI/Public/Models/Models/ModelResult.swift @@ -13,7 +13,7 @@ public struct ModelResult: Decodable, Equatable { public let object: String public let ownedBy: String - enum CodingKeys: String, CodingKey { + public enum CodingKeys: String, CodingKey { case id case object case ownedBy = "owned_by" diff --git a/Sources/OpenAI/Public/Models/Models/ModelsResult.swift b/Sources/OpenAI/Public/Models/Models/ModelsResult.swift index 1f6f5c67..86ca6217 100644 --- a/Sources/OpenAI/Public/Models/Models/ModelsResult.swift +++ b/Sources/OpenAI/Public/Models/Models/ModelsResult.swift @@ -11,4 +11,9 @@ public struct ModelsResult: Decodable, Equatable { public let data: [ModelResult] public let object: String + + public enum CodingKeys: CodingKey { + case data + case object + } } diff --git a/Sources/OpenAI/Public/Models/ModerationsResult.swift b/Sources/OpenAI/Public/Models/ModerationsResult.swift index 70c6af3b..83a92b58 100644 --- a/Sources/OpenAI/Public/Models/ModerationsResult.swift +++ b/Sources/OpenAI/Public/Models/ModerationsResult.swift @@ -27,7 +27,7 @@ public struct ModerationsResult: Decodable, Equatable { /// Violent content that depicts death, violence, or serious physical injury in extreme graphic detail. public let violenceGraphic: Bool - enum CodingKeys: String, CodingKey { + public enum CodingKeys: String, CodingKey { case hate case hateThreatening = "hate/threatening" case selfHarm = "self-harm" @@ -54,7 +54,7 @@ public struct ModerationsResult: Decodable, Equatable { /// Violent content that depicts death, violence, or serious physical injury in extreme graphic detail. public let violenceGraphic: Double - enum CodingKeys: String, CodingKey { + public enum CodingKeys: String, CodingKey { case hate case hateThreatening = "hate/threatening" case selfHarm = "self-harm" @@ -72,7 +72,7 @@ public struct ModerationsResult: Decodable, Equatable { /// True if the model classifies the content as violating OpenAI's usage policies, false otherwise. public let flagged: Bool - enum CodingKeys: String, CodingKey { + public enum CodingKeys: String, CodingKey { case categories case categoryScores = "category_scores" case flagged @@ -82,4 +82,10 @@ public struct ModerationsResult: Decodable, Equatable { public let id: String public let model: Model public let results: [CategoryResult] + + public enum CodingKeys: CodingKey { + case id + case model + case results + } } diff --git a/Tests/OpenAITests/Mocks/EncodableExtensions.swift b/Tests/OpenAITests/Mocks/EncodableExtensions.swift new file mode 100644 index 00000000..519798e0 --- /dev/null +++ b/Tests/OpenAITests/Mocks/EncodableExtensions.swift @@ -0,0 +1,244 @@ +// +// EncodableExtensions.swift +// +// +// Created by James J Kalafus on 2024-02-14. +// + +import OpenAI + +extension APIError: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.message, forKey: .message) + try container.encode(self.type, forKey: .type) + try container.encodeIfPresent(self.param, forKey: .param) + try container.encodeIfPresent(self.code, forKey: .code) + } +} + +extension CompletionsResult: Encodable { + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.id, forKey: .id) + try container.encode(self.object, forKey: .object) + try container.encode(self.created, forKey: .created) + try container.encode(self.model, forKey: .model) + try container.encode(self.choices, forKey: .choices) + try container.encodeIfPresent(self.usage, forKey: .usage) + } +} + +extension CompletionsResult.Choice: Encodable { + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.text, forKey: .text) + try container.encode(self.index, forKey: .index) + try container.encodeIfPresent(self.finishReason, forKey: .finishReason) + } +} + +extension CompletionsResult.Usage: Encodable { + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.promptTokens, forKey: . promptTokens) + try container.encode(self.completionTokens, forKey: . completionTokens) + try container.encode(self.totalTokens, forKey: .totalTokens) + } +} + +extension ChatResult: Encodable { + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.id, forKey: .id) + try container.encode(self.object, forKey: .object) + try container.encode(self.created, forKey: .created) + try container.encode(self.model, forKey: .model) + try container.encode(self.choices, forKey: .choices) + try container.encodeIfPresent(self.usage, forKey: .usage) + } +} + +extension ChatResult.Choice: Encodable { + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.index, forKey: .index) + try container.encode(self.message, forKey: .message) + try container.encodeIfPresent(self.finishReason, forKey: . finishReason) + } +} + +extension ChatResult.Usage: Encodable { + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.promptTokens, forKey: . promptTokens) + try container.encode(self.completionTokens, forKey: . completionTokens) + try container.encode(self.totalTokens, forKey: .totalTokens) + } +} + +extension EditsResult: Encodable { + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.object, forKey: .object) + try container.encode(self.created, forKey: .created) + try container.encode(self.choices, forKey: .choices) + try container.encode(self.usage, forKey: .usage) + } +} + +extension EditsResult.Choice: Encodable { + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.text, forKey: .text) + try container.encode(self.index, forKey: .index) + } +} + +extension EditsResult.Usage: Encodable { + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.promptTokens, forKey: . promptTokens) + try container.encode(self.completionTokens, forKey: . completionTokens) + try container.encode(self.totalTokens, forKey: .totalTokens) + } +} + +extension EmbeddingsResult: Encodable { + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.data, forKey: .data) + try container.encode(self.model, forKey: .model) + try container.encode(self.usage, forKey: .usage) + } +} + +extension EmbeddingsResult.Embedding: Encodable { + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.object, forKey: .object) + try container.encode(self.embedding, forKey: .embedding) + try container.encode(self.index, forKey: .index) + } +} + +extension EmbeddingsResult.Usage: Encodable { + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.promptTokens, forKey: . promptTokens) + try container.encode(self.totalTokens, forKey: .totalTokens) + } +} + +extension ImagesResult: Encodable { + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.created, forKey: .created) + try container.encode(self.data, forKey: .data) + } +} + +extension ImagesResult.URLResult: Encodable { + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(self.url, forKey: .url) + try container.encodeIfPresent(self.b64_json, forKey: . b64_json) + } +} + +extension ModelResult: Encodable { + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.id, forKey: .id) + try container.encode(self.object, forKey: .object) + try container.encode(self.ownedBy, forKey: .ownedBy) + } +} + +extension ModelsResult: Encodable { + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.data, forKey: .data) + try container.encode(self.object, forKey: .object) + } +} + +extension ModerationsResult: Encodable { + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.id, forKey: .id) + try container.encode(self.model, forKey: .model) + try container.encode(self.results, forKey: .results) + } +} + +extension ModerationsResult.CategoryResult: Encodable { + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.categories, forKey: .categories) + try container.encode(self.categoryScores, forKey: . categoryScores) + try container.encode(self.flagged, forKey: .flagged) + } +} + +extension ModerationsResult.CategoryResult.Categories: Encodable { + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.hate, forKey: .hate) + try container.encode(self.hateThreatening, forKey: . hateThreatening) + try container.encode(self.selfHarm, forKey: .selfHarm) + try container.encode(self.sexual, forKey: .sexual) + try container.encode(self.sexualMinors, forKey: . sexualMinors) + try container.encode(self.violence, forKey: .violence) + try container.encode(self.violenceGraphic, forKey: . violenceGraphic) + } +} + +extension ModerationsResult.CategoryResult.CategoryScores: Encodable { + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.hate, forKey: .hate) + try container.encode(self.hateThreatening, forKey: . hateThreatening) + try container.encode(self.selfHarm, forKey: .selfHarm) + try container.encode(self.sexual, forKey: .sexual) + try container.encode(self.sexualMinors, forKey: . sexualMinors) + try container.encode(self.violence, forKey: .violence) + try container.encode(self.violenceGraphic, forKey: . violenceGraphic) + } +} + +extension AudioTranscriptionResult: Encodable { + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.text, forKey: .text) + } +} + +extension AudioTranslationResult: Encodable { + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.text, forKey: .text) + } +} diff --git a/Tests/OpenAITests/Mocks/URLSessionMock.swift b/Tests/OpenAITests/Mocks/URLSessionMock.swift index b3f5a228..5883913a 100644 --- a/Tests/OpenAITests/Mocks/URLSessionMock.swift +++ b/Tests/OpenAITests/Mocks/URLSessionMock.swift @@ -15,12 +15,12 @@ class URLSessionMock: URLSessionProtocol { var dataTask: DataTaskMock! - func dataTask(with request: URLRequest, completionHandler: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTaskProtocol { + public func dataTask(with request: URLRequest, completionHandler: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTaskProtocol { dataTask.completion = completionHandler return dataTask } - func dataTask(with request: URLRequest) -> URLSessionDataTaskProtocol { + public func dataTask(with request: URLRequest) -> URLSessionDataTaskProtocol { dataTask } } diff --git a/Tests/OpenAITests/OpenAITests.swift b/Tests/OpenAITests/OpenAITests.swift index 8af1b45f..bb76a67a 100644 --- a/Tests/OpenAITests/OpenAITests.swift +++ b/Tests/OpenAITests/OpenAITests.swift @@ -394,7 +394,7 @@ extension OpenAITests { self.urlSession.dataTask = task } - func stub(result: Decodable) throws { + func stub(result: Encodable) throws { let encoder = JSONEncoder() let data = try encoder.encode(result) let task = DataTaskMock.successful(with: data) diff --git a/Tests/OpenAITests/OpenAITestsCombine.swift b/Tests/OpenAITests/OpenAITestsCombine.swift index 542d8430..b564bf08 100644 --- a/Tests/OpenAITests/OpenAITestsCombine.swift +++ b/Tests/OpenAITests/OpenAITestsCombine.swift @@ -136,7 +136,7 @@ extension OpenAITestsCombine { self.urlSession.dataTask = task } - func stub(result: Decodable) throws { + func stub(result: Encodable) throws { let encoder = JSONEncoder() let data = try encoder.encode(result) let task = DataTaskMock.successful(with: data)