diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/ScryfallKit.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/ScryfallKit.xcscheme
index ef3943c..33504ad 100644
--- a/.swiftpm/xcode/xcshareddata/xcschemes/ScryfallKit.xcscheme
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/ScryfallKit.xcscheme
@@ -51,11 +51,6 @@
BlueprintName = "ScryfallKitTests"
ReferencedContainer = "container:">
-
-
-
-
diff --git a/Package.resolved b/Package.resolved
index 3b6d58b..528fbc1 100644
--- a/Package.resolved
+++ b/Package.resolved
@@ -1,16 +1,14 @@
{
- "object": {
- "pins": [
- {
- "package": "SwiftDocCPlugin",
- "repositoryURL": "https://github.com/apple/swift-docc-plugin",
- "state": {
- "branch": null,
- "revision": "3303b164430d9a7055ba484c8ead67a52f7b74f6",
- "version": "1.0.0"
- }
+ "pins" : [
+ {
+ "identity" : "swift-docc-plugin",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-docc-plugin",
+ "state" : {
+ "revision" : "3303b164430d9a7055ba484c8ead67a52f7b74f6",
+ "version" : "1.0.0"
}
- ]
- },
- "version": 1
+ }
+ ],
+ "version" : 2
}
diff --git a/Package.swift b/Package.swift
index 0fc648c..47e3d0b 100644
--- a/Package.swift
+++ b/Package.swift
@@ -12,12 +12,13 @@ let package = Package(
targets: ["ScryfallKit"])
],
dependencies: [
- .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0")
+ .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
],
targets: [
.target(
name: "ScryfallKit",
- dependencies: []),
+ dependencies: [
+ ]),
.testTarget(
name: "ScryfallKitTests",
dependencies: ["ScryfallKit"])
diff --git a/README.md b/README.md
index dc25829..82cebf7 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,14 @@ import ScryfallKit
let client = ScryfallClient()
// Retrieve the Strixhaven Mystical Archive printing of Doom Blade
+do {
+ let doomBlade = try await client.getCardByName(exact: "Doom Blade", set: "STA")
+ print(doomBlade.cmc)
+} catch {
+ print("Received error: \(error)")
+}
+
+// Or using a completion handler
client.getCardByName(exact: "Doom Blade", set: "STA") { result in
switch result {
case .success(let doomBlade):
@@ -41,14 +49,6 @@ client.getCardByName(exact: "Doom Blade", set: "STA") { result in
print("Received error: \(error)")
}
}
-
-// Or using async
-do {
- let doomBlade = try await client.getCardByName(exact: "Doom Blade", set: "STA")
- print(doomBlade.cmc)
-} catch {
- print("Received error: \(error)")
-}
```
## Network Logging
diff --git a/Sources/ScryfallKit/Documentation.docc/MultiFacedCards.md b/Sources/ScryfallKit/Documentation.docc/MultiFacedCards.md
index 7a93c67..ca0267a 100644
--- a/Sources/ScryfallKit/Documentation.docc/MultiFacedCards.md
+++ b/Sources/ScryfallKit/Documentation.docc/MultiFacedCards.md
@@ -2,6 +2,10 @@
Dealing with cards that have multiple faces
+@Metadata {
+ @PageKind(sampleCode)
+}
+
## Overview
Magic: the Gathering has a variety of card layouts that are considered "multi-face". These layouts are often identified by an accompanying game mechanic like Transform. Another, less obvious, example would be cards with a "split" layout such as [Armed // Dangerous](https://scryfall.com/card/dgm/122/armed-dangerous) or [Appeal // Authority](https://scryfall.com/card/hou/152/appeal-authority). As each card face is treated like a separate card, each face may have different values for core fields. Mana cost, card type, and power and toughness are some examples. Scryfall has written about this topic themselves [here](https://scryfall.com/docs/api/layouts#card-faces).
diff --git a/Sources/ScryfallKit/Documentation.docc/RetrievingCards.md b/Sources/ScryfallKit/Documentation.docc/RetrievingCards.md
index 2979068..ba079fb 100644
--- a/Sources/ScryfallKit/Documentation.docc/RetrievingCards.md
+++ b/Sources/ScryfallKit/Documentation.docc/RetrievingCards.md
@@ -2,9 +2,13 @@
How to retrieve one or more cards from Scryfall
+@Metadata {
+ @PageKind(sampleCode)
+}
+
## By Searching
-ScryfallKit provides two ways to searching for cards:
+ScryfallKit provides two ways to search for cards:
- Using the ``ScryfallKit/CardFieldFilter`` enum
- Using a string that contains valid [Scryfall Syntax](https://scryfall.com/docs/syntax)
@@ -46,14 +50,7 @@ print(narsetEnlightenedMaster.collectorNumber) // Prints "190"
```
## By Identifier
-There are a number of ways to identify an individual Magic: the Gathering card using the different card properties. Scryfall allows you to identify a card based on the following fields
-- [Scryfall ID](Card/Identifier/scryfallID(id:))
-- [MTGO ID](Card/Identifier/mtgoID(id:))
-- [Multiverse ID](Card/Identifier/multiverseID(id:))
-- [Arena ID](Card/Identifier/arenaID(id:))
-- [TCGPlayer ID](Card/Identifier/tcgPlayerID(id:))
-- [Cardmarket ID](Card/Identifier/cardMarketID(id:))
-- [Set code, collector #, and language](Card/Identifier/setCodeCollectorNo(setCode:collectorNo:lang:))
+Scryfall's data contains the unique IDs used by several other services and marketplaces. In addition, cards are uniquely identifiable by the combination of their set code, collector number, and language. These identifiers are reflected in ``Card/Identifier``
You can use these identifiers to retrieve individual cards with ``ScryfallKit/ScryfallClient/getCard(identifier:completion:)`` or to retrieve up to 75 cards at once with ``ScryfallClient/getCardCollection(identifiers:completion:)``
diff --git a/Sources/ScryfallKit/Extensions/Card+helpers.swift b/Sources/ScryfallKit/Extensions/Card+helpers.swift
index 8211fae..3889d4d 100644
--- a/Sources/ScryfallKit/Extensions/Card+helpers.swift
+++ b/Sources/ScryfallKit/Extensions/Card+helpers.swift
@@ -6,6 +6,7 @@
//
import Foundation
+import OSLog
public extension Card {
/// Get the legality of a card in a given format
@@ -82,7 +83,11 @@ public extension Card {
}
guard let uri = uris.uri(for: type) else {
- print("No URI for image type \(type)")
+ if #available(iOS 14.0, macOS 11.0, *) {
+ Logger.main.error("No URI for image type \(type.rawValue)")
+ } else {
+ print("No URI for image type \(type)")
+ }
return nil
}
diff --git a/Sources/ScryfallKit/Logger.swift b/Sources/ScryfallKit/Logger.swift
new file mode 100644
index 0000000..33f2f21
--- /dev/null
+++ b/Sources/ScryfallKit/Logger.swift
@@ -0,0 +1,16 @@
+//
+// Logger.swift
+//
+
+import Foundation
+import OSLog
+
+@available(macOS 11.0, *)
+@available(iOS 14.0, *)
+extension Logger {
+ static let subsystem = "dev.hearst.scryfallkit"
+ static let main = Logger(subsystem: subsystem, category: "ScryfallKit")
+ static let client = Logger(subsystem: subsystem, category: "ScryfallClient")
+ static let network = Logger(subsystem: subsystem, category: "Network")
+ static let decoder = Logger(subsystem: subsystem, category: "Decoder")
+}
diff --git a/Sources/ScryfallKit/Models/Card/Card+enums.swift b/Sources/ScryfallKit/Models/Card/Card+enums.swift
index c487056..695395a 100644
--- a/Sources/ScryfallKit/Models/Card/Card+enums.swift
+++ b/Sources/ScryfallKit/Models/Card/Card+enums.swift
@@ -3,6 +3,7 @@
//
import Foundation
+import OSLog
extension Card {
/// A value or combination of values that uniquely identify a Magic card
@@ -138,7 +139,11 @@ extension Card {
public init(from decoder: Decoder) throws {
self = (try? Self(rawValue: decoder.singleValueContainer().decode(RawValue.self))) ?? .unknown
if self == .unknown, let rawValue = try? String(from: decoder) {
- print("Decoded unknown FrameEffect: \(rawValue)")
+ if #available(iOS 14.0, macOS 11.0, *) {
+ Logger.decoder.error("Decoded unknown Layout: \(rawValue)")
+ } else {
+ print("Decoded unknown Layout: \(rawValue)")
+ }
}
}
}
@@ -156,8 +161,10 @@ extension Card {
public var label: String {
switch self {
- case .notLegal: return "Not Legal"
- default: return rawValue.capitalized
+ case .notLegal:
+ return "Not Legal"
+ default:
+ return rawValue.capitalized
}
}
}
@@ -193,12 +200,16 @@ extension Card {
///
/// [Scryfall documentation](https://scryfall.com/docs/api/frames#frame-effects)
public enum FrameEffect: String, Codable, CaseIterable {
- case legendary, miracle, nyxtouched, draft, devoid, tombstone, colorshifted, inverted, sunmoondfc, compasslanddfc, originpwdfc, mooneldrazidfc, waxingandwaningmoondfc, showcase, extendedart, companion, etched, snow, lesson, convertdfc, fandfc, battle, unknown
+ case legendary, miracle, nyxtouched, draft, devoid, tombstone, colorshifted, inverted, sunmoondfc, compasslanddfc, originpwdfc, mooneldrazidfc, waxingandwaningmoondfc, showcase, extendedart, companion, etched, snow, lesson, convertdfc, fandfc, battle, gravestone, fullart, unknown
public init(from decoder: Decoder) throws {
self = try FrameEffect(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .unknown
if self == .unknown, let rawValue = try? String(from: decoder) {
- print("Decoded unknown FrameEffect: \(rawValue)")
+ if #available(iOS 14.0, macOS 11.0, *) {
+ Logger.decoder.error("Decoded unknown FrameEffect: \(rawValue)")
+ } else {
+ print("Decoded unknown FrameEffect: \(rawValue)")
+ }
}
}
}
diff --git a/Sources/ScryfallKit/Models/MTGSet.swift b/Sources/ScryfallKit/Models/MTGSet.swift
index c598a0c..3ee9a7b 100644
--- a/Sources/ScryfallKit/Models/MTGSet.swift
+++ b/Sources/ScryfallKit/Models/MTGSet.swift
@@ -3,6 +3,7 @@
//
import Foundation
+import OSLog
/// A set represents a group of related Magic cards
///
@@ -42,7 +43,11 @@ public struct MTGSet: Codable, Identifiable, Hashable {
public init(from decoder: Decoder) throws {
self = try Self(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .unknown
if self == .unknown, let rawValue = try? String(from: decoder) {
- print("Decoded unknown MTGSet Type: \(rawValue)")
+ if #available(iOS 14.0, macOS 11.0, *) {
+ Logger.main.warning("Decoded unknown MTGSet Type: \(rawValue)")
+ } else {
+ print("Decoded unknown MTGSet Type: \(rawValue)")
+ }
}
}
}
diff --git a/Sources/ScryfallKit/Networking/EndpointRequests/CardRequests.swift b/Sources/ScryfallKit/Networking/EndpointRequests/CardRequests.swift
index c2c084d..c1b90db 100644
--- a/Sources/ScryfallKit/Networking/EndpointRequests/CardRequests.swift
+++ b/Sources/ScryfallKit/Networking/EndpointRequests/CardRequests.swift
@@ -3,11 +3,12 @@
//
import Foundation
+import OSLog
struct SearchCards: EndpointRequest {
var body: Data?
var requestMethod: RequestMethod = .GET
- var path: String? = "cards/search"
+ var path = "cards/search"
var queryParams: [URLQueryItem]
init(query: String,
@@ -35,7 +36,7 @@ struct SearchCards: EndpointRequest {
struct GetCardNamed: EndpointRequest {
var body: Data?
var requestMethod: RequestMethod = .GET
- var path: String? = "cards/named"
+ var path = "cards/named"
var queryParams: [URLQueryItem]
init(exact: String? = nil, fuzzy: String? = nil, set: String? = nil) {
@@ -50,7 +51,7 @@ struct GetCardNamed: EndpointRequest {
struct GetCardAutocomplete: EndpointRequest {
var body: Data?
var requestMethod: RequestMethod = .GET
- var path: String? = "cards/autocomplete"
+ var path = "cards/autocomplete"
var queryParams: [URLQueryItem]
init(query: String, includeExtras: Bool? = nil) {
@@ -64,7 +65,7 @@ struct GetCardAutocomplete: EndpointRequest {
struct GetRandomCard: EndpointRequest {
var body: Data?
var requestMethod: RequestMethod = .GET
- var path: String? = "cards/random"
+ var path = "cards/random"
var queryParams: [URLQueryItem]
init(query: String?) {
@@ -77,7 +78,7 @@ struct GetRandomCard: EndpointRequest {
struct GetCard: EndpointRequest {
let identifier: Card.Identifier
- var path: String? {
+ var path: String {
switch identifier {
case .scryfallID(let id):
return "cards/\(id)"
@@ -88,8 +89,13 @@ struct GetCard: EndpointRequest {
default:
// This guard should never trip. The only card identifier that doesn't have provider/id is the set code/collector
guard let id = identifier.id else {
- print("Provided identifier doesn't have a provider or doesn't have an id")
- return nil
+ if #available(iOS 14.0, macOS 11.0, *) {
+ Logger.main.error("Provided identifier doesn't have a provider or doesn't have an id")
+ } else {
+ print("Provided identifier doesn't have a provider or doesn't have an id")
+ }
+
+ fatalError("Encountered a situation that shouldn't be possible: Card identifier's id property was nil")
}
return "cards/\(identifier.provider)/\(id)"
@@ -102,7 +108,7 @@ struct GetCard: EndpointRequest {
}
struct GetCardCollection: EndpointRequest {
- var path: String? = "cards/collection"
+ var path = "cards/collection"
var queryParams: [URLQueryItem] = []
var requestMethod: RequestMethod = .POST
var body: Data?
@@ -114,7 +120,11 @@ struct GetCardCollection: EndpointRequest {
do {
body = try JSONSerialization.data(withJSONObject: requestBody)
} catch {
- print("Errored serializing dict to JSON for GetCardCollection request")
+ if #available(iOS 14.0, macOS 11.0, *) {
+ Logger.main.error("Errored serializing dict to JSON for GetCardCollection request")
+ } else {
+ print("Errored serializing dict to JSON for GetCardCollection request")
+ }
}
}
}
diff --git a/Sources/ScryfallKit/Networking/EndpointRequests/CatalogRequests.swift b/Sources/ScryfallKit/Networking/EndpointRequests/CatalogRequests.swift
index 66d90ba..678cb44 100644
--- a/Sources/ScryfallKit/Networking/EndpointRequests/CatalogRequests.swift
+++ b/Sources/ScryfallKit/Networking/EndpointRequests/CatalogRequests.swift
@@ -7,7 +7,7 @@ import Foundation
struct GetCatalog: EndpointRequest {
var catalogType: Catalog.`Type`
- var path: String? {
+ var path: String {
return "catalog/\(catalogType.rawValue)"
}
diff --git a/Sources/ScryfallKit/Networking/EndpointRequests/EndpointRequest.swift b/Sources/ScryfallKit/Networking/EndpointRequests/EndpointRequest.swift
index 8ce2525..362656d 100644
--- a/Sources/ScryfallKit/Networking/EndpointRequests/EndpointRequest.swift
+++ b/Sources/ScryfallKit/Networking/EndpointRequests/EndpointRequest.swift
@@ -3,9 +3,10 @@
//
import Foundation
+import OSLog
protocol EndpointRequest {
- var path: String? { get }
+ var path: String { get }
var queryParams: [URLQueryItem] { get }
var requestMethod: RequestMethod { get }
var body: Data? { get }
@@ -13,18 +14,17 @@ protocol EndpointRequest {
extension EndpointRequest {
var urlRequest: URLRequest? {
- guard let path = path else {
- print("Couldn't get path for request")
- return nil
- }
-
var urlComponents = URLComponents(string: "https://api.scryfall.com/\(path)")
if !queryParams.isEmpty {
urlComponents?.queryItems = queryParams.compactMap { $0.value == nil ? nil : $0 }
}
guard let url = urlComponents?.url else {
- print("Couldn't make url")
+ if #available(iOS 14.0, macOS 11.0, *) {
+ Logger.main.error("Couldn't make url")
+ } else {
+ print("Couldn't make url")
+ }
return nil
}
diff --git a/Sources/ScryfallKit/Networking/EndpointRequests/RulingsRequests.swift b/Sources/ScryfallKit/Networking/EndpointRequests/RulingsRequests.swift
index 8bde8fe..d566f26 100644
--- a/Sources/ScryfallKit/Networking/EndpointRequests/RulingsRequests.swift
+++ b/Sources/ScryfallKit/Networking/EndpointRequests/RulingsRequests.swift
@@ -7,7 +7,7 @@ import Foundation
struct GetRulings: EndpointRequest {
var identifier: Card.Ruling.Identifier
- var path: String? {
+ var path: String {
switch identifier {
case .multiverseID(let id):
return "cards/multiverse/\(id)/rulings"
diff --git a/Sources/ScryfallKit/Networking/EndpointRequests/SetRequests.swift b/Sources/ScryfallKit/Networking/EndpointRequests/SetRequests.swift
index d51878d..fa9f004 100644
--- a/Sources/ScryfallKit/Networking/EndpointRequests/SetRequests.swift
+++ b/Sources/ScryfallKit/Networking/EndpointRequests/SetRequests.swift
@@ -5,7 +5,7 @@
import Foundation
struct GetSets: EndpointRequest {
- var path: String? = "sets"
+ var path = "sets"
var queryParams: [URLQueryItem] = []
var requestMethod: RequestMethod = .GET
var body: Data?
@@ -14,7 +14,7 @@ struct GetSets: EndpointRequest {
struct GetSet: EndpointRequest {
var identifier: MTGSet.Identifier
- var path: String? {
+ var path: String {
switch self.identifier {
case .code(let code):
return "sets/\(code)"
diff --git a/Sources/ScryfallKit/Networking/EndpointRequests/SymbolRequests.swift b/Sources/ScryfallKit/Networking/EndpointRequests/SymbolRequests.swift
index fe7932c..9ab14d1 100644
--- a/Sources/ScryfallKit/Networking/EndpointRequests/SymbolRequests.swift
+++ b/Sources/ScryfallKit/Networking/EndpointRequests/SymbolRequests.swift
@@ -5,7 +5,7 @@
import Foundation
struct GetSymbology: EndpointRequest {
- var path: String? = "symbology"
+ var path = "symbology"
var queryParams: [URLQueryItem] = []
var requestMethod: RequestMethod = .GET
var body: Data?
@@ -16,7 +16,7 @@ struct ParseManaCost: EndpointRequest {
var cost: String
// Protocol vars
- var path: String? = "symbology/parse-mana"
+ var path = "symbology/parse-mana"
var queryParams: [URLQueryItem]
var requestMethod: RequestMethod = .GET
var body: Data?
diff --git a/Sources/ScryfallKit/Networking/NetworkService.swift b/Sources/ScryfallKit/Networking/NetworkService.swift
index 857e2db..b496b3e 100644
--- a/Sources/ScryfallKit/Networking/NetworkService.swift
+++ b/Sources/ScryfallKit/Networking/NetworkService.swift
@@ -3,6 +3,7 @@
//
import Foundation
+import OSLog
/// An enum representing the two available levels of log verbosity
public enum NetworkLogLevel {
@@ -23,14 +24,22 @@ struct NetworkService: NetworkServiceProtocol {
func request(_ request: EndpointRequest, as type: T.Type, completion: @escaping (Result) -> Void) {
guard let urlRequest = request.urlRequest else {
- print("Invalid url request")
+ if #available(macOS 11.0, iOS 14.0, *) {
+ Logger.network.error("Invalid url request")
+ } else {
+ print("Invalid url request")
+ }
completion(.failure(ScryfallKitError.invalidUrl))
return
}
if logLevel == .verbose, let body = urlRequest.httpBody, let JSONString = String(data: body, encoding: String.Encoding.utf8) {
print("Sending request with body:")
- print(JSONString)
+ if #available(macOS 11.0, iOS 14.0, *) {
+ Logger.network.debug("\(JSONString)")
+ } else {
+ print(JSONString)
+ }
}
let task = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
@@ -42,7 +51,11 @@ struct NetworkService: NetworkServiceProtocol {
}
}
- print("Making request to: '\(String(describing: urlRequest.url?.absoluteString))'")
+ if #available(macOS 11.0, iOS 14.0, *) {
+ Logger.network.debug("Making request to: '\(String(describing: urlRequest.url?.absoluteString))'")
+ } else {
+ print("Making request to: '\(String(describing: urlRequest.url?.absoluteString))'")
+ }
task.resume()
}
@@ -52,12 +65,10 @@ struct NetworkService: NetworkServiceProtocol {
}
guard let content = data else {
- print("Data was nil")
throw ScryfallKitError.noDataReturned
}
guard let httpStatus = (response as? HTTPURLResponse)?.statusCode else {
- print("Couldn't cast httpStatus property of response to HTTPURLResponse")
throw ScryfallKitError.failedToCast("httpStatus property of response to HTTPURLResponse")
}
@@ -67,13 +78,16 @@ struct NetworkService: NetworkServiceProtocol {
if (200..<300).contains(httpStatus) {
if logLevel == .verbose {
let responseBody = String(data: content, encoding: .utf8)
- print(responseBody ?? "Couldn't represent response body as string")
+ if #available(macOS 11.0, iOS 14.0, *) {
+ Logger.network.debug("\(responseBody ?? "Couldn't represent response body as string")")
+ } else {
+ print(responseBody ?? "Couldn't represent response body as string")
+ }
}
return try decoder.decode(dataType, from: content)
} else {
let httpError = try decoder.decode(ScryfallError.self, from: content)
- print(httpError)
throw ScryfallKitError.scryfallError(httpError)
}
}
diff --git a/Tests/ScryfallKitTests/ComparableTests.swift b/Tests/ScryfallKitTests/ComparableTests.swift
index 5d3254f..140063c 100644
--- a/Tests/ScryfallKitTests/ComparableTests.swift
+++ b/Tests/ScryfallKitTests/ComparableTests.swift
@@ -3,7 +3,7 @@
//
import XCTest
-@testable import ScryfallKit
+import ScryfallKit
class ComparableTests: XCTestCase {
func testColorSort() {
diff --git a/Tests/ScryfallKitTests/SmokeTests.swift b/Tests/ScryfallKitTests/SmokeTests.swift
index 7ae6be5..e11eea3 100644
--- a/Tests/ScryfallKitTests/SmokeTests.swift
+++ b/Tests/ScryfallKitTests/SmokeTests.swift
@@ -18,7 +18,7 @@ final class SmokeTests: XCTestCase {
// Skip double sided because there aren't any double_sided or battle cards being returned by Scryfall
for layout in Card.Layout.allCases where ![.doubleSided, .unknown, .battle].contains(layout) {
let cards = try await client.searchCards(query: "layout:\(layout.rawValue)")
- XCTAssertFalse(cards.data.isEmpty)
+ checkForUnknowns(in: cards.data)
}
}
@@ -160,25 +160,25 @@ final class SmokeTests: XCTestCase {
// Search
var results = try await client.searchCards(filters: [filter])
- try checkForUnknowns(in: results.data)
+ checkForUnknowns(in: results.data)
var page = 1
// Go through every page
while results.hasMore ?? false {
page += 1
results = try await client.searchCards(filters: [filter], page: page)
- try checkForUnknowns(in: results.data)
+ checkForUnknowns(in: results.data)
usleep(500000) // Wait for 0.5 seconds
}
}
- private func checkForUnknowns(in cards: [Card]) throws {
+ private func checkForUnknowns(in cards: [Card]) {
for card in cards {
XCTAssertNotEqual(card.layout, .unknown, "Unknown layout on \(card.name)")
XCTAssertNotEqual(card.setType, .unknown, "Unknown set type on \(card.name)")
if let frameEffects = card.frameEffects {
for effect in frameEffects {
- XCTAssertNotEqual(effect, .unknown, "Unknown frame effect on \(card.name)")
+ XCTAssertNotEqual(effect, .unknown, "Unknown frame effect on \(card.name) [\(card.set)]")
}
}
}