From abc7971f6229226bbbadab75651908e1d30e814a Mon Sep 17 00:00:00 2001 From: Sean Kelley Date: Sun, 8 Mar 2026 12:31:50 -0700 Subject: [PATCH 1/6] Add EUR prices in foil and etched. --- Sources/ScryfallKit/Extensions/Card+helpers.swift | 6 ++++-- Sources/ScryfallKit/Models/Card/Card+Prices.swift | 10 ++++++++-- Sources/ScryfallKit/Models/Enums.swift | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Sources/ScryfallKit/Extensions/Card+helpers.swift b/Sources/ScryfallKit/Extensions/Card+helpers.swift index 1fe6035..c1be5fc 100644 --- a/Sources/ScryfallKit/Extensions/Card+helpers.swift +++ b/Sources/ScryfallKit/Extensions/Card+helpers.swift @@ -65,10 +65,12 @@ extension Card { public func getPrice(for currency: Currency) -> String? { switch currency { case .usd: return prices.usd - case .eur: return prices.eur - case .tix: return prices.tix case .usdFoil: return prices.usdFoil case .usdEtched: return prices.usdEtched + case .eur: return prices.eur + case .eurFoil: return prices.eurFoil + case .eurEtched: return prices.eurEtched + case .tix: return prices.tix } } } diff --git a/Sources/ScryfallKit/Models/Card/Card+Prices.swift b/Sources/ScryfallKit/Models/Card/Card+Prices.swift index 31d3048..06096f1 100644 --- a/Sources/ScryfallKit/Models/Card/Card+Prices.swift +++ b/Sources/ScryfallKit/Models/Card/Card+Prices.swift @@ -13,16 +13,22 @@ extension Card { public var usdFoil: String? /// The price of this card's etched printing in usd public var usdEtched: String? - /// The price of this card in eur. + /// The price of this card in eur public var eur: String? + /// The price of this card's foil printing in eur + public var eurFoil: String? + /// The price of this card's etched printing in eur + public var eurEtched: String? - public init(tix: String? = nil, usd: String? = nil, usdFoil: String? = nil, usdEtched: String? = nil, eur: String? = nil) + public init(tix: String? = nil, usd: String? = nil, usdFoil: String? = nil, usdEtched: String? = nil, eur: String? = nil, eurFoil: String? = nil, eurEtched: String? = nil) { self.tix = tix self.usd = usd self.usdFoil = usdFoil self.usdEtched = usdEtched self.eur = eur + self.eurFoil = eurFoil + self.eurEtched = eurEtched } } } diff --git a/Sources/ScryfallKit/Models/Enums.swift b/Sources/ScryfallKit/Models/Enums.swift index dd2ad27..043ca8f 100644 --- a/Sources/ScryfallKit/Models/Enums.swift +++ b/Sources/ScryfallKit/Models/Enums.swift @@ -39,5 +39,5 @@ public enum Format: String, CaseIterable, Sendable { /// Currency types that Scryfall provides prices for public enum Currency: String, CaseIterable, Sendable { - case usd, eur, tix, usdFoil, usdEtched + case usd, usdFoil, usdEtched, eur, eurFoil, eurEtched, tix } From c34b4440d9ee27d40484638841c6b0071f2475ad Mon Sep 17 00:00:00 2001 From: Sean Kelley Date: Fri, 3 Apr 2026 16:32:54 -0700 Subject: [PATCH 2/6] Faces can have flavor names separate from the card. Example: Scryfall ID 54c92a0d-ab6e-45b2-b7a8-e13a5dd4e74e (https://scryfall.com/card/sld/1612/tovolar-dire-overlord-tovolar-the-midnight-scourge) --- Sources/ScryfallKit/Models/Card/Card+Face.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/ScryfallKit/Models/Card/Card+Face.swift b/Sources/ScryfallKit/Models/Card/Card+Face.swift index bc080a2..547ad24 100644 --- a/Sources/ScryfallKit/Models/Card/Card+Face.swift +++ b/Sources/ScryfallKit/Models/Card/Card+Face.swift @@ -25,6 +25,8 @@ extension Card { public var colors: [Card.Color]? /// This face's defense, if any public var defense: String? + /// The just-for-fun name printed on the card (such as for Godzilla series cards). + public var flavorName: String? /// This card's flavor text if any public var flavorText: String? /// An ID for this card face's art that remains consistent across reprints @@ -64,6 +66,7 @@ extension Card { colorIndicator: [Card.Color]? = nil, colors: [Card.Color]? = nil, defense: String? = nil, + flavorName: String? = nil, flavorText: String? = nil, illustrationId: UUID? = nil, imageUris: ImageUris? = nil, @@ -83,6 +86,7 @@ extension Card { self.colorIndicator = colorIndicator self.colors = colors self.defense = defense + self.flavorName = flavorName self.flavorText = flavorText self.illustrationId = illustrationId self.imageUris = imageUris From 7381f4adcf6124400580d3caca1d149408d2f51f Mon Sep 17 00:00:00 2001 From: Sean Kelley Date: Mon, 6 Apr 2026 14:52:26 -0700 Subject: [PATCH 3/6] Add Codable to models that were missing it. --- Sources/ScryfallKit/Models/Enums.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ScryfallKit/Models/Enums.swift b/Sources/ScryfallKit/Models/Enums.swift index 043ca8f..c3d90a2 100644 --- a/Sources/ScryfallKit/Models/Enums.swift +++ b/Sources/ScryfallKit/Models/Enums.swift @@ -31,13 +31,13 @@ public enum SortDirection: String, Codable, CaseIterable, Sendable { } /// Formats for playing Magic: the Gathering -public enum Format: String, CaseIterable, Sendable { +public enum Format: String, Codable, CaseIterable, Sendable { case standard, future, historic, timeless, gladiator, pioneer, modern, legacy, pauper, vintage, penny, commander, oathbreaker, standardbrawl, brawl, alchemy, paupercommander, duel, oldschool, premodern, predh } /// Currency types that Scryfall provides prices for -public enum Currency: String, CaseIterable, Sendable { +public enum Currency: String, Codable, CaseIterable, Sendable { case usd, usdFoil, usdEtched, eur, eurFoil, eurEtched, tix } From 27ab28caf2dfee8f2ed465dfc6a6e7c58350c97e Mon Sep 17 00:00:00 2001 From: Sean Kelley Date: Wed, 8 Apr 2026 12:31:53 -0700 Subject: [PATCH 4/6] Log all HTTP responses and throw more informative errors. --- .../Networking/NetworkService.swift | 25 +++++++++++++------ Sources/ScryfallKit/ScryfallKitError.swift | 6 +++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/Sources/ScryfallKit/Networking/NetworkService.swift b/Sources/ScryfallKit/Networking/NetworkService.swift index 1e2a01a..0bff14e 100644 --- a/Sources/ScryfallKit/Networking/NetworkService.swift +++ b/Sources/ScryfallKit/Networking/NetworkService.swift @@ -52,23 +52,32 @@ struct NetworkService: NetworkServiceProtocol, Sendable { throw error } - guard let content = data else { - throw ScryfallKitError.noDataReturned - } - guard let httpStatus = (response as? HTTPURLResponse)?.statusCode else { throw ScryfallKitError.failedToCast("httpStatus property of response to HTTPURLResponse") } + logger?.debug("HTTP \(httpStatus): \(data.flatMap { String(data: $0, encoding: .utf8) } ?? "Couldn't represent response body as string")") + + guard let content = data else { + throw ScryfallKitError.noDataReturned + } + let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase if (200..<300).contains(httpStatus) { - let responseBody = String(data: content, encoding: .utf8) - logger?.debug("\(responseBody ?? "Couldn't represent response body as string")") - return try decoder.decode(dataType, from: content) + do { + return try decoder.decode(dataType, from: content) + } catch { + throw ScryfallKitError.failedToDecode(content) + } } else { - let httpError = try decoder.decode(ScryfallError.self, from: content) + let httpError: ScryfallError + do { + httpError = try decoder.decode(ScryfallError.self, from: content) + } catch { + throw ScryfallKitError.httpError(httpStatus, content) + } throw ScryfallKitError.scryfallError(httpError) } } diff --git a/Sources/ScryfallKit/ScryfallKitError.swift b/Sources/ScryfallKit/ScryfallKitError.swift index e5693a7..dbbd335 100644 --- a/Sources/ScryfallKit/ScryfallKitError.swift +++ b/Sources/ScryfallKit/ScryfallKitError.swift @@ -13,6 +13,8 @@ public enum ScryfallKitError: LocalizedError, CustomStringConvertible { case singleFacedCard case noDataReturned case failedToCast(String) + case failedToDecode(Data) + case httpError(Int, Data) public var errorDescription: String? { description @@ -30,6 +32,10 @@ public enum ScryfallKitError: LocalizedError, CustomStringConvertible { return "No data was returned by the server" case .failedToCast(let details): return "Failed to cast \(details)" + case .failedToDecode: + return "Failed to decode response into valid JSON" + case .httpError(let statusCode, _): + return "Failed with HTTP \(statusCode)" } } } From c44573ff10d729d695209f4ef7a2a48a3b0d494d Mon Sep 17 00:00:00 2001 From: Sean Kelley Date: Wed, 6 May 2026 15:42:59 -0700 Subject: [PATCH 5/6] Add Identifiable to RelatedCard. --- Sources/ScryfallKit/Models/Card/Card+RelatedCard.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ScryfallKit/Models/Card/Card+RelatedCard.swift b/Sources/ScryfallKit/Models/Card/Card+RelatedCard.swift index 3f69359..e7b14cf 100644 --- a/Sources/ScryfallKit/Models/Card/Card+RelatedCard.swift +++ b/Sources/ScryfallKit/Models/Card/Card+RelatedCard.swift @@ -8,7 +8,7 @@ extension Card { /// A Magic card that's related to another Magic card /// /// - Note: In the documentation of this struct, "this card" will refer to the `RelatedCard` object while "the original card" will refer to the `Card` object that contains this object - public struct RelatedCard: Codable, Hashable, Sendable { + public struct RelatedCard: Codable, Identifiable, Hashable, Sendable { /// The type of relationship public enum Component: String, Codable, CaseIterable, Hashable, Sendable { /// This card is a token that's made by the original card From 12cfe057ddadb811eece2d6a7d83c39cbdfe94b1 Mon Sep 17 00:00:00 2001 From: Sean Kelley Date: Tue, 26 May 2026 11:14:09 -0700 Subject: [PATCH 6/6] Add support for custom User-Agent, required by Scryfall's TOS. --- Sources/ScryfallKit/Networking/NetworkService.swift | 10 ++++++++-- Sources/ScryfallKit/ScryfallClient.swift | 8 +++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Sources/ScryfallKit/Networking/NetworkService.swift b/Sources/ScryfallKit/Networking/NetworkService.swift index 0bff14e..c748080 100644 --- a/Sources/ScryfallKit/Networking/NetworkService.swift +++ b/Sources/ScryfallKit/Networking/NetworkService.swift @@ -15,9 +15,11 @@ protocol NetworkServiceProtocol: Sendable { } struct NetworkService: NetworkServiceProtocol, Sendable { + let userAgent: String? let logger: Logger? - init(logger: Logger?) { + init(userAgent: String?, logger: Logger?) { + self.userAgent = userAgent self.logger = logger } @@ -25,12 +27,16 @@ struct NetworkService: NetworkServiceProtocol, Sendable { _ request: EndpointRequest, as type: T.Type, completion: @Sendable @escaping (Result) -> Void ) { - guard let urlRequest = request.urlRequest else { + guard var urlRequest = request.urlRequest else { logger?.error("Invalid url request") completion(.failure(ScryfallKitError.invalidUrl)) return } + if let userAgent { + urlRequest.setValue(userAgent, forHTTPHeaderField: "User-Agent") + } + logger?.trace("Starting request: \(urlRequest.debugDescription)") let task = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in do { diff --git a/Sources/ScryfallKit/ScryfallClient.swift b/Sources/ScryfallKit/ScryfallClient.swift index 3b226aa..4ee32dc 100644 --- a/Sources/ScryfallKit/ScryfallClient.swift +++ b/Sources/ScryfallKit/ScryfallClient.swift @@ -10,9 +10,11 @@ public final class ScryfallClient: Sendable { let networkService: NetworkServiceProtocol /// Initialize an instance of the ScryfallClient - /// - Parameter logger: The logger to use. Pass nil to disable logging - public init(logger: Logger? = nil) { - self.networkService = NetworkService(logger: logger) + /// - Parameters: + /// - userAgent: The value for the HTTP User-Agent header; required by Scryfall's API usage terms + /// - logger: The logger to use. Pass nil to disable logging + public init(userAgent: String? = nil, logger: Logger? = nil) { + self.networkService = NetworkService(userAgent: userAgent, logger: logger) } /// Perform a search using an array of ``CardFieldFilter`` objects.