diff --git a/Sources/ScryfallKit/Models/Card/Card+RelatedCard.swift b/Sources/ScryfallKit/Models/Card/Card+RelatedCard.swift index e7b14cf..bf9d42d 100644 --- a/Sources/ScryfallKit/Models/Card/Card+RelatedCard.swift +++ b/Sources/ScryfallKit/Models/Card/Card+RelatedCard.swift @@ -10,15 +10,30 @@ extension 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, Identifiable, Hashable, Sendable { /// The type of relationship - public enum Component: String, Codable, CaseIterable, Hashable, Sendable { + public enum Component: RawRepresentable, Codable, CaseIterable, Hashable, Sendable, Equatable { /// This card is a token that's made by the original card case token /// This card melds with the original card - case meldPart = "meld_part" + case meldPart /// This card is the result of melding the original card with its other half - case meldResult = "meld_result" + case meldResult /// This card combos with the original card - case comboPiece = "combo_piece" + case comboPiece + /// A relationship that hasn't been added to ScryfallKit yet + case unknown(String) + + /// All known relationships + public static let allCases: [Component] = [.token, .meldPart, .meldResult, .comboPiece] + + public var rawValue: String { + switch self { + case .meldPart: "meld_part" + case .meldResult: "meld_result" + case .comboPiece: "combo_piece" + case .unknown(let unknownRawValue): unknownRawValue + default: String(describing: self) + } + } } /// The Scryfall ID of this card diff --git a/Sources/ScryfallKit/Models/Card/Card+Ruling.swift b/Sources/ScryfallKit/Models/Card/Card+Ruling.swift index 259b0d4..25e64bb 100644 --- a/Sources/ScryfallKit/Models/Card/Card+Ruling.swift +++ b/Sources/ScryfallKit/Models/Card/Card+Ruling.swift @@ -17,9 +17,21 @@ extension Card { } /// A computer-readable string indicating which company produced this ruling - public enum Source: String, Codable, Sendable { + public enum Source: RawRepresentable, Codable, CaseIterable, Sendable, Equatable, Hashable { case scryfall case wotc + /// A source that hasn't been added to ScryfallKit yet + case unknown(String) + + /// All known ruling sources + public static let allCases: [Source] = [.scryfall, .wotc] + + public var rawValue: String { + switch self { + case .unknown(let unknownRawValue): unknownRawValue + default: String(describing: self) + } + } } /// A computer-readable string indicating which company produced this ruling diff --git a/Sources/ScryfallKit/Models/Card/Card+enums.swift b/Sources/ScryfallKit/Models/Card/Card+enums.swift index c9da400..1951bb0 100644 --- a/Sources/ScryfallKit/Models/Card/Card+enums.swift +++ b/Sources/ScryfallKit/Models/Card/Card+enums.swift @@ -89,8 +89,20 @@ extension Card { } /// Finishes for a printed card - public enum Finish: String, Codable, CaseIterable, Sendable { + public enum Finish: RawRepresentable, Codable, CaseIterable, Sendable, Equatable, Hashable { case nonfoil, foil, etched, glossy + /// A finish that hasn't been added to ScryfallKit yet + case unknown(String) + + /// All known finishes + public static let allCases: [Card.Finish] = [.nonfoil, .foil, .etched, .glossy] + + public var rawValue: String { + switch self { + case .unknown(let unknownRawValue): unknownRawValue + default: String(describing: self) + } + } } /// Status of Scryfall's image asset for this card @@ -146,14 +158,14 @@ extension Card { /// [Scryfall documentation](https://scryfall.com/docs/api/layouts) public enum Layout: RawRepresentable, CaseIterable, Codable, Sendable, Equatable, Hashable { case normal, split, flip, transform, meld, leveler, saga, adventure, planar, scheme, vanguard, - token, emblem, augment, host, `class`, battle, `case`, mutate, prototype, modalDfc, doubleSided, doubleFacedToken, artSeries, reversibleCard + token, emblem, augment, host, `class`, battle, `case`, mutate, prototype, prepare, modalDfc, doubleSided, doubleFacedToken, artSeries, reversibleCard, frontCard /// A layout that hasn't been added to ScryfallKit yet case unknown(String) /// All known Magic: the Gathering card layouts public static let allCases: [Card.Layout] = [ - .normal, .split, .flip, .transform, .meld, .leveler, .saga, .adventure, .planar, .scheme, .vanguard, .token, .emblem, .augment, .host, .class, .battle, .case, .mutate, .prototype, .modalDfc, .doubleSided, .doubleFacedToken, .artSeries, .reversibleCard, + .normal, .split, .flip, .transform, .meld, .leveler, .saga, .adventure, .planar, .scheme, .vanguard, .token, .emblem, .augment, .host, .class, .battle, .case, .mutate, .prototype, .prepare, .modalDfc, .doubleSided, .doubleFacedToken, .artSeries, .reversibleCard, .frontCard, ] public var rawValue: String { @@ -162,6 +174,7 @@ extension Card { case .doubleSided: "double_sided" case .doubleFacedToken: "double_faced_token" case .artSeries: "art_series" + case .frontCard: "front_card" case .reversibleCard: "reversible_card" case .unknown(let string): string default: String(describing: self) @@ -201,20 +214,72 @@ extension Card { } } + /// A value that a card can produce, as reported by `producedMana` + /// + /// As of this writing, only one card (Unfinity's Sole Performer) can produce "mana" that isn't a + /// normal color, so this enumeration is split from the core Color enumeration for convenience in + /// the overwhelmingly common cases. + public enum ProducedColor: RawRepresentable, Codable, CaseIterable, Sendable, Equatable, Hashable + { + // swiftlint:disable:next identifier_name + case W, U, B, R, G, C + /// A produced value that isn't a color, or that hasn't been added to ScryfallKit yet + case unknown(String) + + /// All the produced values that are also colors, in the order Scryfall sorts them + public static let allCases: [Card.ProducedColor] = [.W, .U, .B, .R, .G, .C] + + public var rawValue: String { + switch self { + case .unknown(let unknownRawValue): unknownRawValue + default: String(describing: self) + } + } + + /// The equivalent ``Card/Color``, or nil if this value isn't one of Magic's colors + public var color: Color? { Color(rawValue: rawValue) } + } + /// Card border colors - public enum BorderColor: String, Codable, CaseIterable, Sendable { + public enum BorderColor: RawRepresentable, Codable, CaseIterable, Sendable, Equatable, Hashable { case black, borderless, gold, silver, white, yellow + /// A border color that hasn't been added to ScryfallKit yet + case unknown(String) + + /// All known border colors + public static let allCases: [Card.BorderColor] = [ + .black, .borderless, .gold, .silver, .white, .yellow, + ] + + public var rawValue: String { + switch self { + case .unknown(let unknownRawValue): unknownRawValue + default: String(describing: self) + } + } } /// Card frames /// /// [Scryfall documentation](https://scryfall.com/docs/api/frames) - public enum Frame: String, Codable, CaseIterable, Sendable { - case v1993 = "1993" - case v1997 = "1997" - case v2003 = "2003" - case v2015 = "2015" - case future + public enum Frame: RawRepresentable, Codable, CaseIterable, Sendable, Equatable, Hashable { + case v1993, v1997, v2003, v2015, future + /// A frame that hasn't been added to ScryfallKit yet + case unknown(String) + + /// All known frames + public static let allCases: [Card.Frame] = [.v1993, .v1997, .v2003, .v2015, .future] + + public var rawValue: String { + switch self { + case .v1993: "1993" + case .v1997: "1997" + case .v2003: "2003" + case .v2015: "2015" + case .unknown(let unknownRawValue): unknownRawValue + default: String(describing: self) + } + } } /// Effects applied to a Magic card frame diff --git a/Sources/ScryfallKit/Models/Card/Card.swift b/Sources/ScryfallKit/Models/Card/Card.swift index 76875b6..d05fcb5 100644 --- a/Sources/ScryfallKit/Models/Card/Card.swift +++ b/Sources/ScryfallKit/Models/Card/Card.swift @@ -96,7 +96,7 @@ public struct Card: Codable, Identifiable, Hashable, Sendable { /// The power of this card if it's a creature public var power: String? /// The colors of mana that this card _could_ produce - public var producedMana: [Color]? + public var producedMana: [ProducedColor]? /// True if this card is on the Reserved List public var reserved: Bool /// The toughness of this card if it's a creature @@ -238,7 +238,7 @@ public struct Card: Codable, Identifiable, Hashable, Sendable { pennyRank: Int? = nil, oversized: Bool, power: String? = nil, - producedMana: [Color]? = nil, + producedMana: [ProducedColor]? = nil, reserved: Bool, toughness: String? = nil, typeLine: String? = nil, diff --git a/Sources/ScryfallKit/Models/Enums.swift b/Sources/ScryfallKit/Models/Enums.swift index c3d90a2..8cf7fb0 100644 --- a/Sources/ScryfallKit/Models/Enums.swift +++ b/Sources/ScryfallKit/Models/Enums.swift @@ -5,8 +5,20 @@ import Foundation /// Environments to play Magic: The Gathering in -public enum Game: String, Codable, CaseIterable, Sendable { +public enum Game: RawRepresentable, Codable, CaseIterable, Sendable, Equatable, Hashable { case paper, mtgo, arena, astral, sega + /// A game that hasn't been added to ScryfallKit yet + case unknown(String) + + /// All known games + public static let allCases: [Game] = [.paper, .mtgo, .arena, .astral, .sega] + + public var rawValue: String { + switch self { + case .unknown(let unknownRawValue): unknownRawValue + default: String(describing: self) + } + } } /// Comparison strategies for determining what makes a card "unique" diff --git a/Sources/ScryfallKit/Models/UnknownDecodable.swift b/Sources/ScryfallKit/Models/UnknownDecodable.swift index 2627b80..1f0b5fa 100644 --- a/Sources/ScryfallKit/Models/UnknownDecodable.swift +++ b/Sources/ScryfallKit/Models/UnknownDecodable.swift @@ -22,7 +22,14 @@ extension UnknownDecodable { } } +extension Card.BorderColor: UnknownDecodable {} +extension Card.Finish: UnknownDecodable {} +extension Card.Frame: UnknownDecodable {} extension Card.FrameEffect: UnknownDecodable {} +extension Card.ProducedColor: UnknownDecodable {} extension Card.Layout: UnknownDecodable {} +extension Card.RelatedCard.Component: UnknownDecodable {} +extension Card.Ruling.Source: UnknownDecodable {} extension Card.SecurityStamp: UnknownDecodable {} +extension Game: UnknownDecodable {} extension MTGSet.Kind: UnknownDecodable {} diff --git a/Tests/ScryfallKitTests/CaseIterableTests.swift b/Tests/ScryfallKitTests/CaseIterableTests.swift index 68ab57b..04549f7 100644 --- a/Tests/ScryfallKitTests/CaseIterableTests.swift +++ b/Tests/ScryfallKitTests/CaseIterableTests.swift @@ -97,11 +97,13 @@ final class CaseIterableTests: XCTestCase { case .case: Card.Layout.allCases.contains(.case) case .mutate: Card.Layout.allCases.contains(.mutate) case .prototype: Card.Layout.allCases.contains(.prototype) + case .prepare: Card.Layout.allCases.contains(.prepare) case .modalDfc: Card.Layout.allCases.contains(.modalDfc) case .doubleSided: Card.Layout.allCases.contains(.doubleSided) case .doubleFacedToken: Card.Layout.allCases.contains(.doubleFacedToken) case .artSeries: Card.Layout.allCases.contains(.artSeries) case .reversibleCard: Card.Layout.allCases.contains(.reversibleCard) + case .frontCard: Card.Layout.allCases.contains(.frontCard) case .unknown(let string): // Unknown case shouldn't be in allCases !Card.Layout.allCases.contains(.unknown(string)) @@ -110,6 +112,115 @@ final class CaseIterableTests: XCTestCase { XCTAssertTrue(contains) } + func testFinish() { + let stub = Card.Finish.foil + let contains = switch stub { + case .nonfoil: Card.Finish.allCases.contains(.nonfoil) + case .foil: Card.Finish.allCases.contains(.foil) + case .etched: Card.Finish.allCases.contains(.etched) + case .glossy: Card.Finish.allCases.contains(.glossy) + case .unknown(let string): + // Unknown case shouldn't be in allCases + !Card.Finish.allCases.contains(.unknown(string)) + } + + XCTAssertTrue(contains) + } + + func testBorderColor() { + let stub = Card.BorderColor.black + let contains = switch stub { + case .black: Card.BorderColor.allCases.contains(.black) + case .borderless: Card.BorderColor.allCases.contains(.borderless) + case .gold: Card.BorderColor.allCases.contains(.gold) + case .silver: Card.BorderColor.allCases.contains(.silver) + case .white: Card.BorderColor.allCases.contains(.white) + case .yellow: Card.BorderColor.allCases.contains(.yellow) + case .unknown(let string): + // Unknown case shouldn't be in allCases + !Card.BorderColor.allCases.contains(.unknown(string)) + } + + XCTAssertTrue(contains) + } + + func testFrame() { + let stub = Card.Frame.v2015 + let contains = switch stub { + case .v1993: Card.Frame.allCases.contains(.v1993) + case .v1997: Card.Frame.allCases.contains(.v1997) + case .v2003: Card.Frame.allCases.contains(.v2003) + case .v2015: Card.Frame.allCases.contains(.v2015) + case .future: Card.Frame.allCases.contains(.future) + case .unknown(let string): + // Unknown case shouldn't be in allCases + !Card.Frame.allCases.contains(.unknown(string)) + } + + XCTAssertTrue(contains) + } + + func testGame() { + let stub = Game.paper + let contains = switch stub { + case .paper: Game.allCases.contains(.paper) + case .mtgo: Game.allCases.contains(.mtgo) + case .arena: Game.allCases.contains(.arena) + case .astral: Game.allCases.contains(.astral) + case .sega: Game.allCases.contains(.sega) + case .unknown(let string): + // Unknown case shouldn't be in allCases + !Game.allCases.contains(.unknown(string)) + } + + XCTAssertTrue(contains) + } + + func testComponent() { + let stub = Card.RelatedCard.Component.token + let contains = switch stub { + case .token: Card.RelatedCard.Component.allCases.contains(.token) + case .meldPart: Card.RelatedCard.Component.allCases.contains(.meldPart) + case .meldResult: Card.RelatedCard.Component.allCases.contains(.meldResult) + case .comboPiece: Card.RelatedCard.Component.allCases.contains(.comboPiece) + case .unknown(let string): + // Unknown case shouldn't be in allCases + !Card.RelatedCard.Component.allCases.contains(.unknown(string)) + } + + XCTAssertTrue(contains) + } + + func testRulingSource() { + let stub = Card.Ruling.Source.wotc + let contains = switch stub { + case .scryfall: Card.Ruling.Source.allCases.contains(.scryfall) + case .wotc: Card.Ruling.Source.allCases.contains(.wotc) + case .unknown(let string): + // Unknown case shouldn't be in allCases + !Card.Ruling.Source.allCases.contains(.unknown(string)) + } + + XCTAssertTrue(contains) + } + + func testProducedColor() { + let stub = Card.ProducedColor.W + let contains = switch stub { + case .W: Card.ProducedColor.allCases.contains(.W) + case .U: Card.ProducedColor.allCases.contains(.U) + case .B: Card.ProducedColor.allCases.contains(.B) + case .R: Card.ProducedColor.allCases.contains(.R) + case .G: Card.ProducedColor.allCases.contains(.G) + case .C: Card.ProducedColor.allCases.contains(.C) + case .unknown(let string): + // Unknown case shouldn't be in allCases + !Card.ProducedColor.allCases.contains(.unknown(string)) + } + + XCTAssertTrue(contains) + } + func testSecurityStamp() { let stub = Card.SecurityStamp.oval let contains = switch stub { diff --git a/Tests/ScryfallKitTests/SmokeTests.swift b/Tests/ScryfallKitTests/SmokeTests.swift index 9071e3a..de626c8 100644 --- a/Tests/ScryfallKitTests/SmokeTests.swift +++ b/Tests/ScryfallKitTests/SmokeTests.swift @@ -174,6 +174,9 @@ final class SmokeTests: XCTestCase { } private func checkForUnknowns(in cards: [Card]) { + // Note that `producedMana` is deliberately not checked here: an unknown ProducedColor is + // expected rather than a gap in ScryfallKit, because Scryfall reports values that aren't + // colors at all (Unfinity's Sole Performer produces "T"). for card in cards { if let frameEffects = card.frameEffects { for effect in frameEffects { @@ -182,6 +185,20 @@ final class SmokeTests: XCTestCase { } } } + + if case .unknown(let string) = card.layout { + XCTFail("Unknown layout: \(string) on \(card.name)") + } + + for face in card.cardFaces ?? [] { + if case .unknown(let string) = face.layout { + XCTFail("Unknown face layout: \(string) on \(card.name)") + } + } + + if case .unknown(let string) = card.setType { + XCTFail("Unknown set type: \(string) on \(card.name)") + } } } }