diff --git a/Package.swift b/Package.swift index 7580394..1550d16 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.9 +// swift-tools-version:6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -20,6 +20,7 @@ let package = Package( ), .testTarget( name: "ScryfallKitTests", - dependencies: ["ScryfallKit"]) + dependencies: ["ScryfallKit"] + ) ] ) diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift new file mode 100644 index 0000000..c6f5e4c --- /dev/null +++ b/Package@swift-5.9.swift @@ -0,0 +1,26 @@ +// swift-tools-version:5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "ScryfallKit", + platforms: [.macOS(.v10_13), .iOS(.v12)], + products: [ + .library( + name: "ScryfallKit", + targets: ["ScryfallKit"]) + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), + ], + targets: [ + .target( + name: "ScryfallKit" + ), + .testTarget( + name: "ScryfallKitTests", + dependencies: ["ScryfallKit"] + ) + ] +) diff --git a/Sources/ScryfallKit/Models/Card/Card+Ruling.swift b/Sources/ScryfallKit/Models/Card/Card+Ruling.swift index 865745e..6ee99a7 100644 --- a/Sources/ScryfallKit/Models/Card/Card+Ruling.swift +++ b/Sources/ScryfallKit/Models/Card/Card+Ruling.swift @@ -6,7 +6,7 @@ import Foundation extension Card { /// An object representing a ruling on a specific card - public struct Ruling: Codable, Identifiable { + public struct Ruling: Codable, Identifiable, Sendable { /// A value or combination of values that can identify a ruling. Used to find rulings for specific cards public enum Identifier { case scryfallID(id: String) @@ -17,7 +17,7 @@ extension Card { } /// A computer-readable string indicating which company produced this ruling - public enum Source: String, Codable { + public enum Source: String, Codable, Sendable { case scryfall case wotc } diff --git a/Sources/ScryfallKit/Models/Card/Card+Symbol.swift b/Sources/ScryfallKit/Models/Card/Card+Symbol.swift index 54f0244..a6d108c 100644 --- a/Sources/ScryfallKit/Models/Card/Card+Symbol.swift +++ b/Sources/ScryfallKit/Models/Card/Card+Symbol.swift @@ -6,7 +6,7 @@ import Foundation extension Card { /// A symbol that could appear on a Magic: the Gathering card - public struct Symbol: Codable, Identifiable { + public struct Symbol: Codable, Identifiable, Sendable { /// The textual representation of this symbol public var symbol: String /// A more loose variation of the symbol. diff --git a/Sources/ScryfallKit/Models/ObjectList.swift b/Sources/ScryfallKit/Models/ObjectList.swift index 1b6fb7e..02766e6 100644 --- a/Sources/ScryfallKit/Models/ObjectList.swift +++ b/Sources/ScryfallKit/Models/ObjectList.swift @@ -7,7 +7,7 @@ import Foundation /// A sequence of Scryfall objects. May be paginated /// /// [Scryfall documentation](https://scryfall.com/docs/api/lists) -public struct ObjectList: Codable { +public struct ObjectList: Codable, Sendable where T: Sendable { /// The data contained in the list public var data: [T] /// True if there's a next page, nil if there's only one page diff --git a/Sources/ScryfallKit/Networking/NetworkService.swift b/Sources/ScryfallKit/Networking/NetworkService.swift index 79f8527..c46d699 100644 --- a/Sources/ScryfallKit/Networking/NetworkService.swift +++ b/Sources/ScryfallKit/Networking/NetworkService.swift @@ -1,103 +1,107 @@ // // NetworkService.swift -// +// import Foundation import OSLog /// An enum representing the two available levels of log verbosity public enum NetworkLogLevel: Sendable { - /// Only log when requests are made and errors - case minimal - /// Log the bodies of requests and responses - case verbose + /// Only log when requests are made and errors + case minimal + /// Log the bodies of requests and responses + case verbose } protocol NetworkServiceProtocol: Sendable { - func request(_ request: EndpointRequest, as type: T.Type, completion: @Sendable @escaping (Result) -> Void) - @available(macOS 10.15.0, *, iOS 13.0.0, *) - func request(_ request: EndpointRequest, as type: T.Type) async throws -> T + func request( + _ request: EndpointRequest, + as type: T.Type, + completion: @Sendable @escaping (Result) -> Void + ) + @available(macOS 10.15.0, *, iOS 13.0.0, *) + func request(_ request: EndpointRequest, as type: T.Type) async throws -> T } struct NetworkService: NetworkServiceProtocol, Sendable { - var logLevel: NetworkLogLevel - - func request(_ request: EndpointRequest, as type: T.Type, completion: @Sendable @escaping (Result) -> Void) { - guard let urlRequest = request.urlRequest else { - if #available(macOS 11.0, iOS 14.0, *) { - Logger.network.error("Invalid url request") - } else { - print("Invalid url request") - } - completion(.failure(ScryfallKitError.invalidUrl)) - return - } + var logLevel: NetworkLogLevel - if logLevel == .verbose, let body = urlRequest.httpBody, let JSONString = String(data: body, encoding: String.Encoding.utf8) { - print("Sending request with body:") - if #available(macOS 11.0, iOS 14.0, *) { - Logger.network.debug("\(JSONString)") - } else { - print(JSONString) - } - } + func request(_ request: EndpointRequest, as type: T.Type, completion: @Sendable @escaping (Result) -> Void) { + guard let urlRequest = request.urlRequest else { + if #available(macOS 11.0, iOS 14.0, *) { + Logger.network.error("Invalid url request") + } else { + print("Invalid url request") + } + completion(.failure(ScryfallKitError.invalidUrl)) + return + } - let task = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in - do { - let result = try handle(dataType: type, data: data, response: response, error: error) - completion(.success(result)) - } catch { - completion(.failure(error)) - } - } + if logLevel == .verbose, let body = urlRequest.httpBody, let JSONString = String(data: body, encoding: String.Encoding.utf8) { + print("Sending request with body:") + if #available(macOS 11.0, iOS 14.0, *) { + Logger.network.debug("\(JSONString)") + } else { + print(JSONString) + } + } - 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() + let task = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in + do { + let result = try handle(dataType: type, data: data, response: response, error: error) + completion(.success(result)) + } catch { + completion(.failure(error)) + } } - func handle(dataType: T.Type, data: Data?, response: URLResponse?, error: Error?) throws -> T { - if let error = error { - throw error - } + 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() + } - guard let content = data else { - throw ScryfallKitError.noDataReturned - } + func handle(dataType: T.Type, data: Data?, response: URLResponse?, error: Error?) throws -> T { + if let error = error { + throw error + } - guard let httpStatus = (response as? HTTPURLResponse)?.statusCode else { - throw ScryfallKitError.failedToCast("httpStatus property of response to HTTPURLResponse") - } + guard let content = data else { + throw ScryfallKitError.noDataReturned + } - let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase + guard let httpStatus = (response as? HTTPURLResponse)?.statusCode else { + throw ScryfallKitError.failedToCast("httpStatus property of response to HTTPURLResponse") + } - if (200..<300).contains(httpStatus) { - if logLevel == .verbose { - let responseBody = String(data: content, encoding: .utf8) - 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") - } - } + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase - return try decoder.decode(dataType, from: content) + if (200..<300).contains(httpStatus) { + if logLevel == .verbose { + let responseBody = String(data: content, encoding: .utf8) + if #available(macOS 11.0, iOS 14.0, *) { + Logger.network.debug("\(responseBody ?? "Couldn't represent response body as string")") } else { - let httpError = try decoder.decode(ScryfallError.self, from: content) - throw ScryfallKitError.scryfallError(httpError) + 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) + throw ScryfallKitError.scryfallError(httpError) } + } - @available(macOS 10.15.0, *, iOS 13.0.0, *) - func request(_ request: EndpointRequest, as type: T.Type) async throws -> T { - try await withCheckedThrowingContinuation { continuation in - self.request(request, as: type) { result in - continuation.resume(with: result) - } - } + @available(macOS 10.15.0, *, iOS 13.0.0, *) + func request(_ request: EndpointRequest, as type: T.Type) async throws -> T where T: Sendable { + try await withCheckedThrowingContinuation { continuation in + self.request(request, as: type) { result in + continuation.resume(with: result) + } } + } } diff --git a/Sources/ScryfallKit/ScryfallClient+Async.swift b/Sources/ScryfallKit/ScryfallClient+Async.swift index e729b8b..4ae359e 100644 --- a/Sources/ScryfallKit/ScryfallClient+Async.swift +++ b/Sources/ScryfallKit/ScryfallClient+Async.swift @@ -1,122 +1,126 @@ // // ScryfallClient+Async.swift -// +// import Foundation @available(macOS 10.15.0, *, iOS 13.0.0, *) extension ScryfallClient { - /// Equivalent to ``searchCards(filters:unique:order:sortDirection:includeExtras:includeMultilingual:includeVariations:page:completion:)`` but with async/await syntax - public func searchCards(filters: [CardFieldFilter], - unique: UniqueMode? = nil, - order: SortMode? = nil, - sortDirection: SortDirection? = nil, - includeExtras: Bool? = nil, - includeMultilingual: Bool? = nil, - includeVariations: Bool? = nil, - page: Int? = nil) async throws -> ObjectList { - try await withCheckedThrowingContinuation { continuation in - searchCards(filters: filters, - unique: unique, - order: order, - sortDirection: sortDirection, - includeExtras: includeExtras, - includeMultilingual: includeMultilingual, - includeVariations: includeVariations, - page: page) { result in - continuation.resume(with: result) - } - } - } - - /// Equivalent to ``searchCards(query:unique:order:sortDirection:includeExtras:includeMultilingual:includeVariations:page:completion:)`` but with async/await syntax - public func searchCards(query: String, - unique: UniqueMode? = nil, - order: SortMode? = nil, - sortDirection: SortDirection? = nil, - includeExtras: Bool? = nil, - includeMultilingual: Bool? = nil, - includeVariations: Bool? = nil, - page: Int? = nil) async throws -> ObjectList { - let request = SearchCards(query: query, - unique: unique, - order: order, - dir: sortDirection, - includeExtras: includeExtras, - includeMultilingual: includeMultilingual, - includeVariations: includeVariations, - page: page) - - return try await networkService.request(request, as: ObjectList.self) - } - - /// Equivalent to ``getCardByName(exact:set:completion:)`` but with async/await syntax - public func getCardByName(exact: String, set: String? = nil) async throws -> Card { - let request = GetCardNamed(exact: exact, set: set) - return try await networkService.request(request, as: Card.self) - } - - /// Equivalent to ``getCardByName(fuzzy:set:completion:)`` but with async/await syntax - public func getCardByName(fuzzy: String, set: String? = nil) async throws -> Card { - let request = GetCardNamed(fuzzy: fuzzy, set: set) - return try await networkService.request(request, as: Card.self) - } - - /// Equivalent to ``getCardNameAutocomplete(query:includeExtras:completion:)`` but with async/await syntax - public func getCardNameAutocomplete(query: String, includeExtras: Bool? = nil) async throws -> Catalog { - let request = GetCardAutocomplete(query: query, includeExtras: includeExtras) - return try await networkService.request(request, as: Catalog.self) - } - - /// Equivalent to ``getRandomCard(query:completion:)`` but with async/await syntax - public func getRandomCard(query: String? = nil) async throws -> Card { - let request = GetRandomCard(query: query) - return try await networkService.request(request, as: Card.self) - } - - /// Equivalent to ``getCard(identifier:completion:)`` but with async/await syntax - public func getCard(identifier: Card.Identifier) async throws -> Card { - let request = GetCard(identifier: identifier) - return try await networkService.request(request, as: Card.self) - } - - /// Equivalent to ``getCardCollection(identifiers:completion:)`` but with async/await syntax - public func getCardCollection(identifiers: [Card.CollectionIdentifier]) async throws -> ObjectList { - let request = GetCardCollection(identifiers: identifiers) - return try await networkService.request(request, as: ObjectList.self) - } - - /// Equivalent to ``getCatalog(catalogType:completion:)`` but with async/await syntax - public func getCatalog(catalogType: Catalog.`Type`) async throws -> Catalog { - let request = GetCatalog(catalogType: catalogType) - return try await networkService.request(request, as: Catalog.self) - } - - /// Equivalent to ``getSets(completion:)`` but with async/await syntax - public func getSets() async throws -> ObjectList { - try await networkService.request(GetSets(), as: ObjectList.self) - } - - /// Equivalent to ``getSet(identifier:completion:)`` but with async/await syntax - public func getSet(identifier: MTGSet.Identifier) async throws -> MTGSet { - let request = GetSet(identifier: identifier) - return try await networkService.request(request, as: MTGSet.self) - } - - /// Equivalent to ``getRulings(_:completion:)`` but with async/await syntax - public func getRulings(_ identifier: Card.Ruling.Identifier) async throws -> ObjectList { - let request = GetRulings(identifier: identifier) - return try await networkService.request(request, as: ObjectList.self) - } - - /// Equivalent to ``getSymbology(completion:)`` but with async/await syntax - public func getSymbology() async throws -> ObjectList { - try await networkService.request(GetSymbology(), as: ObjectList.self) - } - - /// Equivalent to ``parseManaCost(_:completion:)`` but with async/await syntax - public func parseManaCost(_ cost: String) async throws -> Card.ManaCost { - let request = ParseManaCost(cost: cost) - return try await networkService.request(request, as: Card.ManaCost.self) + /// Equivalent to ``searchCards(filters:unique:order:sortDirection:includeExtras:includeMultilingual:includeVariations:page:completion:)`` but with async/await syntax + public func searchCards( + filters: [CardFieldFilter], + unique: UniqueMode? = nil, + order: SortMode? = nil, + sortDirection: SortDirection? = nil, + includeExtras: Bool? = nil, + includeMultilingual: Bool? = nil, + includeVariations: Bool? = nil, + page: Int? = nil + ) async throws -> ObjectList { + try await withCheckedThrowingContinuation { continuation in + searchCards( + filters: filters, + unique: unique, + order: order, + sortDirection: sortDirection, + includeExtras: includeExtras, + includeMultilingual: includeMultilingual, + includeVariations: includeVariations, + page: page + ) { result in + continuation.resume(with: result) + } } + } + + /// Equivalent to ``searchCards(query:unique:order:sortDirection:includeExtras:includeMultilingual:includeVariations:page:completion:)`` but with async/await syntax + public func searchCards(query: String, + unique: UniqueMode? = nil, + order: SortMode? = nil, + sortDirection: SortDirection? = nil, + includeExtras: Bool? = nil, + includeMultilingual: Bool? = nil, + includeVariations: Bool? = nil, + page: Int? = nil) async throws -> ObjectList { + let request = SearchCards(query: query, + unique: unique, + order: order, + dir: sortDirection, + includeExtras: includeExtras, + includeMultilingual: includeMultilingual, + includeVariations: includeVariations, + page: page) + + return try await networkService.request(request, as: ObjectList.self) + } + + /// Equivalent to ``getCardByName(exact:set:completion:)`` but with async/await syntax + public func getCardByName(exact: String, set: String? = nil) async throws -> Card { + let request = GetCardNamed(exact: exact, set: set) + return try await networkService.request(request, as: Card.self) + } + + /// Equivalent to ``getCardByName(fuzzy:set:completion:)`` but with async/await syntax + public func getCardByName(fuzzy: String, set: String? = nil) async throws -> Card { + let request = GetCardNamed(fuzzy: fuzzy, set: set) + return try await networkService.request(request, as: Card.self) + } + + /// Equivalent to ``getCardNameAutocomplete(query:includeExtras:completion:)`` but with async/await syntax + public func getCardNameAutocomplete(query: String, includeExtras: Bool? = nil) async throws -> Catalog { + let request = GetCardAutocomplete(query: query, includeExtras: includeExtras) + return try await networkService.request(request, as: Catalog.self) + } + + /// Equivalent to ``getRandomCard(query:completion:)`` but with async/await syntax + public func getRandomCard(query: String? = nil) async throws -> Card { + let request = GetRandomCard(query: query) + return try await networkService.request(request, as: Card.self) + } + + /// Equivalent to ``getCard(identifier:completion:)`` but with async/await syntax + public func getCard(identifier: Card.Identifier) async throws -> Card { + let request = GetCard(identifier: identifier) + return try await networkService.request(request, as: Card.self) + } + + /// Equivalent to ``getCardCollection(identifiers:completion:)`` but with async/await syntax + public func getCardCollection(identifiers: [Card.CollectionIdentifier]) async throws -> ObjectList { + let request = GetCardCollection(identifiers: identifiers) + return try await networkService.request(request, as: ObjectList.self) + } + + /// Equivalent to ``getCatalog(catalogType:completion:)`` but with async/await syntax + public func getCatalog(catalogType: Catalog.`Type`) async throws -> Catalog { + let request = GetCatalog(catalogType: catalogType) + return try await networkService.request(request, as: Catalog.self) + } + + /// Equivalent to ``getSets(completion:)`` but with async/await syntax + public func getSets() async throws -> ObjectList { + try await networkService.request(GetSets(), as: ObjectList.self) + } + + /// Equivalent to ``getSet(identifier:completion:)`` but with async/await syntax + public func getSet(identifier: MTGSet.Identifier) async throws -> MTGSet { + let request = GetSet(identifier: identifier) + return try await networkService.request(request, as: MTGSet.self) + } + + /// Equivalent to ``getRulings(_:completion:)`` but with async/await syntax + public func getRulings(_ identifier: Card.Ruling.Identifier) async throws -> ObjectList { + let request = GetRulings(identifier: identifier) + return try await networkService.request(request, as: ObjectList.self) + } + + /// Equivalent to ``getSymbology(completion:)`` but with async/await syntax + public func getSymbology() async throws -> ObjectList { + try await networkService.request(GetSymbology(), as: ObjectList.self) + } + + /// Equivalent to ``parseManaCost(_:completion:)`` but with async/await syntax + public func parseManaCost(_ cost: String) async throws -> Card.ManaCost { + let request = ParseManaCost(cost: cost) + return try await networkService.request(request, as: Card.ManaCost.self) + } }