diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 0000000..1688426 --- /dev/null +++ b/CONTRIBUTORS @@ -0,0 +1,14 @@ +For the purpose of tracking copyright, this is the list of individuals and +organizations who have contributed source code to this Swift package. + +For employees of an organization/company where the copyright of work done +by employees of that company is held by the company itself, only the company +needs to be listed here. + +## COPYRIGHT HOLDERS + +- Röck+Cöde VoF. (all contributors with '@rock-n-code.com') + +## PROJECT AUTHORS + +- Javier Cicchelli diff --git a/Makefile b/Makefile index 2f62c51..7107182 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,15 @@ +# ===----------------------------------------------------------------------=== +# +# This source file is part of the AmiiboAPI open source project +# +# Copyright (c) 2024 Röck+Cöde VoF. and the AmiiboAPI project authors +# Licensed under the EUPL 1.2 or later. +# +# See LICENSE for license information +# See CONTRIBUTORS for the list of AmiiboAPI project authors +# +# ===----------------------------------------------------------------------=== + # ENVIRONMENT VARIABLES environment ?= .env @@ -39,4 +51,4 @@ package-update: ## Updates the SPM package dependencies help: ## Prints the written documentation for all the defined tasks @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) -.DEFAULT_GOAL := help \ No newline at end of file +.DEFAULT_GOAL := help diff --git a/Package.swift b/Package.swift index 52e5f6d..2a0813b 100644 --- a/Package.swift +++ b/Package.swift @@ -1,9 +1,28 @@ // swift-tools-version: 5.9 +//===----------------------------------------------------------------------=== +// +// This source file is part of the AmiiboAPI open source project +// +// Copyright (c) 2024 Röck+Cöde VoF. and the AmiiboAPI project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of AmiiboAPI project authors +// +//===----------------------------------------------------------------------=== + import PackageDescription let package = Package( name: AmiiboAPI.package, + platforms: [ + .iOS(.v13), + .macOS(.v10_15), + .tvOS(.v13), + .visionOS(.v1), + .watchOS(.v6) + ], products: [ .library( name: AmiiboAPI.package, @@ -12,10 +31,40 @@ let package = Package( ] ) ], + dependencies: [ + .package( + url: "https://github.com/apple/swift-openapi-generator.git", + from: "1.3.0" + ), + .package( + url: "https://github.com/apple/swift-openapi-runtime", + from: "1.5.0" + ), + .package( + url: "https://github.com/apple/swift-openapi-urlsession", + from: "1.0.2" + ) + ], targets: [ .target( name: AmiiboAPI.target, - path: "Sources" + dependencies: [ + .product( + name: "OpenAPIRuntime", + package: "swift-openapi-runtime" + ), + .product( + name: "OpenAPIURLSession", + package: "swift-openapi-urlsession" + ) + ], + path: "Sources", + plugins: [ + .plugin( + name: "OpenAPIGenerator", + package: "swift-openapi-generator" + ), + ] ), .testTarget( name: AmiiboAPI.test, diff --git a/Sources/Internal/Extensions/Client+Properties.swift b/Sources/Internal/Extensions/Client+Properties.swift new file mode 100644 index 0000000..23f090c --- /dev/null +++ b/Sources/Internal/Extensions/Client+Properties.swift @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------=== +// +// This source file is part of the AmiiboAPI open source project +// +// Copyright (c) 2024 Röck+Cöde VoF. and the AmiiboAPI project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of AmiiboAPI project authors +// +//===----------------------------------------------------------------------=== + +import OpenAPIURLSession + +extension Client { + + // MARK: Constants + + static var live: Client { + get throws { + .init( + serverURL: try Servers.server1(), + configuration: .init(dateTranscoder: ISODateTranscoder()), + transport: URLSessionTransport() + ) + } + } + +} diff --git a/Sources/Internal/Extensions/DateFormatter+Properties.swift b/Sources/Internal/Extensions/DateFormatter+Properties.swift new file mode 100644 index 0000000..6c9cce9 --- /dev/null +++ b/Sources/Internal/Extensions/DateFormatter+Properties.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------=== +// +// This source file is part of the AmiiboAPI open source project +// +// Copyright (c) 2024 Röck+Cöde VoF. and the AmiiboAPI project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of AmiiboAPI project authors +// +//===----------------------------------------------------------------------=== + +import Foundation + +extension DateFormatter { + + static var isoDateTime: DateFormatter { + let formatter = DateFormatter() + + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS" + formatter.timeZone = .init(secondsFromGMT: 0) + + return formatter + } + +} diff --git a/Sources/Internal/Protocols/APIClient.swift b/Sources/Internal/Protocols/APIClient.swift new file mode 100644 index 0000000..568f862 --- /dev/null +++ b/Sources/Internal/Protocols/APIClient.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------=== +// +// This source file is part of the AmiiboAPI open source project +// +// Copyright (c) 2024 Röck+Cöde VoF. and the AmiiboAPI project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of AmiiboAPI project authors +// +//===----------------------------------------------------------------------=== + +import Foundation + +public protocol APIClient { + + // MARK: Functions + + func getAmiibos(by filter: AmiiboFilter) async throws -> [Amiibo] + func getAmiiboSeries(by filter: AmiiboSeriesFilter) async throws -> [AmiiboSeries] + func getAmiiboTypes(by filter: AmiiboTypeFilter) async throws -> [AmiiboType] + func getGameCharacters(by filter: GameCharacterFilter) async throws -> [GameCharacter] + func getGameSeries(by filter: GameSeriesFilter) async throws -> [GameSeries] + func getLastUpdated() async throws -> Date + +} diff --git a/Sources/Internal/Protocols/KeyNameFilter.swift b/Sources/Internal/Protocols/KeyNameFilter.swift new file mode 100644 index 0000000..29cd4a4 --- /dev/null +++ b/Sources/Internal/Protocols/KeyNameFilter.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------=== +// +// This source file is part of the AmiiboAPI open source project +// +// Copyright (c) 2024 Röck+Cöde VoF. and the AmiiboAPI project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of AmiiboAPI project authors +// +//===----------------------------------------------------------------------=== + +protocol KeyNameFilter { + + // MARK: Properties + + var key: String? { get } + var name: String? { get } + + // MARK: Initialisers + + init() + init(key: String) + init(name: String) + +} diff --git a/Sources/Internal/Protocols/KeyNameModel.swift b/Sources/Internal/Protocols/KeyNameModel.swift new file mode 100644 index 0000000..f1c023a --- /dev/null +++ b/Sources/Internal/Protocols/KeyNameModel.swift @@ -0,0 +1,24 @@ +//===----------------------------------------------------------------------=== +// +// This source file is part of the AmiiboAPI open source project +// +// Copyright (c) 2024 Röck+Cöde VoF. and the AmiiboAPI project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of AmiiboAPI project authors +// +//===----------------------------------------------------------------------=== + +protocol KeyNameModel: Sendable { + + // MARK: Properties + + var key: String { get } + var name: String { get } + + // MARK: Initialisers + + init(_ payload: Components.Schemas.Tuple) + +} diff --git a/Sources/Internal/Transcoders/ISODateTranscoder.swift b/Sources/Internal/Transcoders/ISODateTranscoder.swift new file mode 100644 index 0000000..2a9a91d --- /dev/null +++ b/Sources/Internal/Transcoders/ISODateTranscoder.swift @@ -0,0 +1,31 @@ +//===----------------------------------------------------------------------=== +// +// This source file is part of the AmiiboAPI open source project +// +// Copyright (c) 2024 Röck+Cöde VoF. and the AmiiboAPI project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of AmiiboAPI project authors +// +//===----------------------------------------------------------------------=== + +import Foundation +import OpenAPIRuntime + +struct ISODateTranscoder: DateTranscoder { + + // MARK: Properties + private let dateFormatter: DateFormatter = .isoDateTime + + // MARK: Functions + + func encode(_ date: Date) throws -> String { + dateFormatter.string(from: date) + } + + func decode(_ string: String) throws -> Date { + dateFormatter.date(from: string) ?? .init() + } + +} diff --git a/Sources/Public/Clients/AmiiboLiveClient.swift b/Sources/Public/Clients/AmiiboLiveClient.swift new file mode 100644 index 0000000..6028575 --- /dev/null +++ b/Sources/Public/Clients/AmiiboLiveClient.swift @@ -0,0 +1,248 @@ +//===----------------------------------------------------------------------=== +// +// This source file is part of the AmiiboAPI open source project +// +// Copyright (c) 2024 Röck+Cöde VoF. and the AmiiboAPI project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of AmiiboAPI project authors +// +//===----------------------------------------------------------------------=== + +import Foundation +import OpenAPIRuntime +import OpenAPIURLSession + +public struct AmiiboLiveClient { + + // MARK: Properties + + private let client: Client + + // MARK: Initialisers + + public init() throws { + self.client = .init( + serverURL: try Servers.server1(), + configuration: .init(dateTranscoder: ISODateTranscoder()), + transport: URLSessionTransport() + ) + } + +} + +// MARK: - APIProtocol + +extension AmiiboLiveClient: APIClient { + + // MARK: Functions + + public func getAmiibos(by filter: AmiiboFilter) async throws -> [Amiibo] { + let response = try await { + do { + return try await client.getAmiibos( + .init(query: .init( + amiiboSeries: filter.series, + character: filter.gameCharacter, + gameseries: filter.gameSeries, + id: filter.identifier, + name: filter.name, + showgames: filter.showGames, + showusage: filter.showUsage, + _type: filter.type + )) + ) + } catch let error as ClientError { + guard let _ = error.underlyingError as? DecodingError else { + throw AmiiboServiceError.unknown + } + + throw AmiiboServiceError.decoding + } catch { + throw AmiiboServiceError.unknown + } + }() + + switch response { + case let .ok(ok): + switch ok.body { + case let .json(output): + return map(output) + } + + case .badRequest: + throw AmiiboServiceError.badRequest + + case let .undocumented(statusCode, _): + throw AmiiboServiceError.undocumented(statusCode) + } + } + + public func getAmiiboSeries(by filter: AmiiboSeriesFilter) async throws -> [AmiiboSeries] { + let response = try await client.getAmiiboSeries( + .init(query: .init( + key: filter.key, + name: filter.name + )) + ) + + switch response { + case let .ok(ok): + switch ok.body { + case let .json(output): + return map(output, as: AmiiboSeries.self) + } + + case .badRequest: + throw AmiiboServiceError.badRequest + + case .internalServerError: + throw AmiiboServiceError.notAvailable + + case .notFound: + throw AmiiboServiceError.notFound + + case let .undocumented(statusCode, _): + throw AmiiboServiceError.undocumented(statusCode) + } + } + + public func getAmiiboTypes(by filter: AmiiboTypeFilter) async throws -> [AmiiboType] { + let response = try await client.getAmiiboTypes( + .init(query: .init( + key: filter.key, + name: filter.name + )) + ) + + switch response { + case let .ok(ok): + switch ok.body { + case let .json(output): + return map(output, as: AmiiboType.self) + } + + case .badRequest: + throw AmiiboServiceError.badRequest + + case .internalServerError: + throw AmiiboServiceError.notAvailable + + case .notFound: + throw AmiiboServiceError.notFound + + case let .undocumented(statusCode, _): + throw AmiiboServiceError.undocumented(statusCode) + } + } + + public func getGameCharacters(by filter: GameCharacterFilter) async throws -> [GameCharacter] { + let response = try await client.getGameCharacters( + .init(query: .init( + key: filter.key, + name: filter.name + )) + ) + + switch response { + case let .ok(ok): + switch ok.body { + case let .json(output): + return map(output, as: GameCharacter.self) + } + + case .badRequest: + throw AmiiboServiceError.badRequest + + case .internalServerError: + throw AmiiboServiceError.notAvailable + + case .notFound: + throw AmiiboServiceError.notFound + + case let .undocumented(statusCode, _): + throw AmiiboServiceError.undocumented(statusCode) + } + } + + public func getGameSeries(by filter: GameSeriesFilter) async throws -> [GameSeries] { + let response = try await client.getGameSeries( + .init(query: .init( + key: filter.key, + name: filter.name + )) + ) + + switch response { + case let .ok(ok): + switch ok.body { + case let .json(output): + return map(output, as: GameSeries.self) + } + + case .badRequest: + throw AmiiboServiceError.badRequest + + case .internalServerError: + throw AmiiboServiceError.notAvailable + + case .notFound: + throw AmiiboServiceError.notFound + + case let .undocumented(statusCode, _): + throw AmiiboServiceError.undocumented(statusCode) + } + } + + public func getLastUpdated() async throws -> Date { + let response = try await client.getLastUpdated() + + switch response { + case let .ok(ok): + switch ok.body { + case let .json(output): + return output.lastUpdated + } + + case let .undocumented(statusCode, _): + throw AmiiboServiceError.undocumented(statusCode) + } + } + +} + +// MARK: - Helpers + +private extension AmiiboLiveClient { + + // MARK: Functions + + func map(_ wrapper: Components.Schemas.AmiiboWrapper) -> [Amiibo] { + switch wrapper.amiibo { + case let .Amiibo(object): + return [.init(object)] + + case let .AmiiboList(list): + return list + .map { .init($0) } + .sorted { $0.identifier < $1.identifier } + } + } + + func map( + _ wrapper: Components.Schemas.TupleWrapper, + as: Model.Type + ) -> [Model] { + switch wrapper.amiibo { + case let .Tuple(payload): + return [.init(payload)] + + case let .TupleList(list): + return list + .map { .init($0) } + .sorted { $0.key < $1.key } + } + } + +} diff --git a/Sources/Public/Clients/AmiiboMockClient.swift b/Sources/Public/Clients/AmiiboMockClient.swift new file mode 100644 index 0000000..dbcb754 --- /dev/null +++ b/Sources/Public/Clients/AmiiboMockClient.swift @@ -0,0 +1,130 @@ +//===----------------------------------------------------------------------=== +// +// This source file is part of the AmiiboAPI open source project +// +// Copyright (c) 2024 Röck+Cöde VoF. and the AmiiboAPI project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of AmiiboAPI project authors +// +//===----------------------------------------------------------------------=== + +import Foundation + +public struct AmiiboMockClient { + + // MARK: Properties + + private let amiibos: [Amiibo]? + private let amiiboSeries: [AmiiboSeries]? + private let amiiboTypes: [AmiiboType]? + private let error: AmiiboServiceError? + private let gameCharacters: [GameCharacter]? + private let gameSeries: [GameSeries]? + private let lastUpdated: Date? + + // MARK: Initialisers + + public init( + amiibos: [Amiibo]? = nil, + amiiboSeries: [AmiiboSeries]? = nil, + amiiboTypes: [AmiiboType]? = nil, + gameCharacters: [GameCharacter]? = nil, + gameSeries: [GameSeries]? = nil, + lastUpdated: Date? = nil, + error: AmiiboServiceError? = nil + ) { + self.amiibos = amiibos + self.amiiboSeries = amiiboSeries + self.amiiboTypes = amiiboTypes + self.error = error + self.gameCharacters = gameCharacters + self.gameSeries = gameSeries + self.lastUpdated = lastUpdated + } + +} + +// MARK: - APIClient + +extension AmiiboMockClient: APIClient { + + // MARK: Functions + + public func getAmiibos(by filter: AmiiboFilter) async throws -> [Amiibo] { + try throwErrorIfExists() + + guard let amiibos else { + throw AmiiboServiceError.notFound + } + + return amiibos + } + + public func getAmiiboSeries(by filter: AmiiboSeriesFilter) async throws -> [AmiiboSeries] { + try throwErrorIfExists() + + guard let amiiboSeries else { + throw AmiiboServiceError.notFound + } + + return amiiboSeries + } + + public func getAmiiboTypes(by filter: AmiiboTypeFilter) async throws -> [AmiiboType] { + try throwErrorIfExists() + + guard let amiiboTypes else { + throw AmiiboServiceError.notFound + } + + return amiiboTypes + } + + public func getGameCharacters(by filter: GameCharacterFilter) async throws -> [GameCharacter] { + try throwErrorIfExists() + + guard let gameCharacters else { + throw AmiiboServiceError.notFound + } + + return gameCharacters + } + + public func getGameSeries(by filter: GameSeriesFilter) async throws -> [GameSeries] { + try throwErrorIfExists() + + guard let gameSeries else { + throw AmiiboServiceError.notFound + } + + return gameSeries + } + + public func getLastUpdated() async throws -> Date { + try throwErrorIfExists() + + guard let lastUpdated else { + throw AmiiboServiceError.notFound + } + + return lastUpdated + } + + +} + +// MARK: - Helpers + +private extension AmiiboMockClient { + + // MARK: Functions + + func throwErrorIfExists() throws { + if let error { + throw error + } + } + +} diff --git a/Sources/Public/Errors/AmiiboServiceError.swift b/Sources/Public/Errors/AmiiboServiceError.swift new file mode 100644 index 0000000..7b59b07 --- /dev/null +++ b/Sources/Public/Errors/AmiiboServiceError.swift @@ -0,0 +1,24 @@ +//===----------------------------------------------------------------------=== +// +// This source file is part of the AmiiboAPI open source project +// +// Copyright (c) 2024 Röck+Cöde VoF. and the AmiiboAPI project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of AmiiboAPI project authors +// +//===----------------------------------------------------------------------=== + +public enum AmiiboServiceError: Error { + case badRequest + case decoding + case notAvailable + case notFound + case undocumented(_ statusCode: Int) + case unknown +} + +// MARK: - Equatable + +extension AmiiboServiceError: Equatable {} diff --git a/Sources/Public/Filters/AmiiboFilter.swift b/Sources/Public/Filters/AmiiboFilter.swift new file mode 100644 index 0000000..4d714be --- /dev/null +++ b/Sources/Public/Filters/AmiiboFilter.swift @@ -0,0 +1,48 @@ +//===----------------------------------------------------------------------=== +// +// This source file is part of the AmiiboAPI open source project +// +// Copyright (c) 2024 Röck+Cöde VoF. and the AmiiboAPI project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of AmiiboAPI project authors +// +//===----------------------------------------------------------------------=== + +public struct AmiiboFilter { + + // MARK: Properties + + public let gameCharacter: String? + public let gameSeries: String? + public let identifier: String? + public let name: String? + public let series: String? + public let showGames: Bool? + public let showUsage: Bool? + public let type: String? + + // MARK: Initialisers + + public init( + identifier: String? = nil, + name: String? = nil, + type: String? = nil, + series: String? = nil, + gameCharacter: String? = nil, + gameSeries: String? = nil, + showGames: Bool? = nil, + showUsage: Bool? = nil + ) { + self.gameCharacter = gameCharacter + self.gameSeries = gameSeries + self.identifier = identifier + self.name = name + self.series = series + self.showGames = showGames + self.showUsage = showUsage + self.type = type + } + +} diff --git a/Sources/Public/Filters/AmiiboSeriesFilter.swift b/Sources/Public/Filters/AmiiboSeriesFilter.swift new file mode 100644 index 0000000..1691904 --- /dev/null +++ b/Sources/Public/Filters/AmiiboSeriesFilter.swift @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------=== +// +// This source file is part of the AmiiboAPI open source project +// +// Copyright (c) 2024 Röck+Cöde VoF. and the AmiiboAPI project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of AmiiboAPI project authors +// +//===----------------------------------------------------------------------=== + +public struct AmiiboSeriesFilter: KeyNameFilter { + + // MARK: Properties + + public let key: String? + public let name: String? + + // MARK: Initialisers + + public init() { + self.key = nil + self.name = nil + } + + public init(key: String) { + self.key = key + self.name = nil + } + + public init(name: String) { + self.key = nil + self.name = name + } + +} diff --git a/Sources/Public/Filters/AmiiboTypeFilter.swift b/Sources/Public/Filters/AmiiboTypeFilter.swift new file mode 100644 index 0000000..438ab51 --- /dev/null +++ b/Sources/Public/Filters/AmiiboTypeFilter.swift @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------=== +// +// This source file is part of the AmiiboAPI open source project +// +// Copyright (c) 2024 Röck+Cöde VoF. and the AmiiboAPI project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of AmiiboAPI project authors +// +//===----------------------------------------------------------------------=== + +public struct AmiiboTypeFilter: KeyNameFilter { + + // MARK: Properties + + public let key: String? + public let name: String? + + // MARK: Initialisers + + public init() { + self.key = nil + self.name = nil + } + + public init(key: String) { + self.key = key + self.name = nil + } + + public init(name: String) { + self.key = nil + self.name = name + } + +} diff --git a/Sources/Public/Filters/GameCharacterFilter.swift b/Sources/Public/Filters/GameCharacterFilter.swift new file mode 100644 index 0000000..0fede7e --- /dev/null +++ b/Sources/Public/Filters/GameCharacterFilter.swift @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------=== +// +// This source file is part of the AmiiboAPI open source project +// +// Copyright (c) 2024 Röck+Cöde VoF. and the AmiiboAPI project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of AmiiboAPI project authors +// +//===----------------------------------------------------------------------=== + +public struct GameCharacterFilter: KeyNameFilter { + + // MARK: Properties + + public let key: String? + public let name: String? + + // MARK: Initialisers + + public init() { + self.key = nil + self.name = nil + } + + public init(key: String) { + self.key = key + self.name = nil + } + + public init(name: String) { + self.key = nil + self.name = name + } + +} diff --git a/Sources/Public/Filters/GameSeriesFilter.swift b/Sources/Public/Filters/GameSeriesFilter.swift new file mode 100644 index 0000000..f02d59c --- /dev/null +++ b/Sources/Public/Filters/GameSeriesFilter.swift @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------=== +// +// This source file is part of the AmiiboAPI open source project +// +// Copyright (c) 2024 Röck+Cöde VoF. and the AmiiboAPI project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of AmiiboAPI project authors +// +//===----------------------------------------------------------------------=== + +public struct GameSeriesFilter: KeyNameFilter { + + // MARK: Properties + + public let key: String? + public let name: String? + + // MARK: Initialisers + + public init() { + self.key = nil + self.name = nil + } + + public init(key: String) { + self.key = key + self.name = nil + } + + public init(name: String) { + self.key = nil + self.name = name + } + +} diff --git a/Sources/Public/Models/Amiibo.swift b/Sources/Public/Models/Amiibo.swift new file mode 100644 index 0000000..1518d9b --- /dev/null +++ b/Sources/Public/Models/Amiibo.swift @@ -0,0 +1,59 @@ +//===----------------------------------------------------------------------=== +// +// This source file is part of the AmiiboAPI open source project +// +// Copyright (c) 2024 Röck+Cöde VoF. and the AmiiboAPI project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of AmiiboAPI project authors +// +//===----------------------------------------------------------------------=== + +import Foundation + +public struct Amiibo: Sendable { + + // MARK: Properties + + public let gameCharacter: String + public let gameSeries: String + public let head: String + public let image: String + public let name: String + public let platform: Platform? + public let release: Release + public let series: String + public let tail: String + public let type: String + + // MARK: Initialisers + + init(_ payload: Components.Schemas.Amiibo) { + self.gameCharacter = payload.character + self.gameSeries = payload.gameSeries + self.head = payload.head + self.image = payload.image + self.name = payload.name + self.platform = .init( + payload.gamesSwitch, + payload.games3DS, + payload.gamesWiiU + ) + self.release = .init(payload.release) + self.series = payload.amiiboSeries + self.tail = payload.tail + self.type = payload._type + } + + // MARK: Computed + + public var identifier: String { + head + tail + } + + public var imageURL: URL? { + .init(string: image) + } + +} diff --git a/Sources/Public/Models/Amiibo/Amiibo+Game.swift b/Sources/Public/Models/Amiibo/Amiibo+Game.swift new file mode 100644 index 0000000..1e2b450 --- /dev/null +++ b/Sources/Public/Models/Amiibo/Amiibo+Game.swift @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------=== +// +// This source file is part of the AmiiboAPI open source project +// +// Copyright (c) 2024 Röck+Cöde VoF. and the AmiiboAPI project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of AmiiboAPI project authors +// +//===----------------------------------------------------------------------=== + +extension Amiibo { + public struct Game: Sendable { + + // MARK: Properties + + public let identifiers: [String] + public let name: String + public let usages: [Usage]? + + // MARK: Initialisers + + init(_ payload: Components.Schemas.AmiiboGame) { + self.identifiers = payload.gameID + self.name = payload.gameName + self.usages = { + guard let usages = payload.amiiboUsage else { + return nil + } + + return usages.map { .init($0) } + }() + } + + } +} diff --git a/Sources/Public/Models/Amiibo/Amiibo+Platform.swift b/Sources/Public/Models/Amiibo/Amiibo+Platform.swift new file mode 100644 index 0000000..66ed86d --- /dev/null +++ b/Sources/Public/Models/Amiibo/Amiibo+Platform.swift @@ -0,0 +1,51 @@ +//===----------------------------------------------------------------------=== +// +// This source file is part of the AmiiboAPI open source project +// +// Copyright (c) 2024 Röck+Cöde VoF. and the AmiiboAPI project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of AmiiboAPI project authors +// +//===----------------------------------------------------------------------=== + +extension Amiibo { + public struct Platform: Sendable { + + // MARK: Properties + + public let `switch`: [Game] + public let threeDS: [Game] + public let wiiU: [Game] + + // MARK: Initialisers + + init?( + _ `switch`: [Components.Schemas.AmiiboGame]?, + _ threeDS: [Components.Schemas.AmiiboGame]?, + _ wiiU: [Components.Schemas.AmiiboGame]? + ) { + guard (`switch` != nil && `switch`?.isEmpty == false) + || (threeDS != nil && threeDS?.isEmpty == false) + || (wiiU != nil && wiiU?.isEmpty == false) + else { + return nil + } + + self.switch = { + guard let `switch` else { return [] } + return `switch`.map { .init($0) } + }() + self.threeDS = { + guard let threeDS else { return [] } + return threeDS.map { .init($0) } + }() + self.wiiU = { + guard let wiiU else { return [] } + return wiiU.map { .init($0) } + }() + } + + } +} diff --git a/Sources/Public/Models/Amiibo/Amiibo+Release.swift b/Sources/Public/Models/Amiibo/Amiibo+Release.swift new file mode 100644 index 0000000..8e5adab --- /dev/null +++ b/Sources/Public/Models/Amiibo/Amiibo+Release.swift @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------=== +// +// This source file is part of the AmiiboAPI open source project +// +// Copyright (c) 2024 Röck+Cöde VoF. and the AmiiboAPI project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of AmiiboAPI project authors +// +//===----------------------------------------------------------------------=== + +import Foundation + +extension Amiibo { + public struct Release: Sendable { + + // MARK: Properties + + public let america: Date? + public let australia: Date? + public let europe: Date? + public let japan: Date? + + // MARK: Initialisers + + init(_ payload: Components.Schemas.AmiiboRelease) { + self.america = payload.na + self.australia = payload.au + self.europe = payload.eu + self.japan = payload.jp + } + + } +} diff --git a/Sources/Public/Models/Amiibo/Amiibo+Usage.swift b/Sources/Public/Models/Amiibo/Amiibo+Usage.swift new file mode 100644 index 0000000..7b738b9 --- /dev/null +++ b/Sources/Public/Models/Amiibo/Amiibo+Usage.swift @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------=== +// +// This source file is part of the AmiiboAPI open source project +// +// Copyright (c) 2024 Röck+Cöde VoF. and the AmiiboAPI project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of AmiiboAPI project authors +// +//===----------------------------------------------------------------------=== + +extension Amiibo { + public struct Usage: Sendable { + + // MARK: Properties + + public let explanation: String + public let isWriteable: Bool + + // MARK: Initialisers + + init(_ payload: Components.Schemas.AmiiboUsage) { + self.explanation = payload.Usage + self.isWriteable = payload.write + } + + } +} diff --git a/Sources/Public/Models/AmiiboSeries.swift b/Sources/Public/Models/AmiiboSeries.swift new file mode 100644 index 0000000..5a6c814 --- /dev/null +++ b/Sources/Public/Models/AmiiboSeries.swift @@ -0,0 +1,27 @@ +//===----------------------------------------------------------------------=== +// +// This source file is part of the AmiiboAPI open source project +// +// Copyright (c) 2024 Röck+Cöde VoF. and the AmiiboAPI project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of AmiiboAPI project authors +// +//===----------------------------------------------------------------------=== + +public struct AmiiboSeries: KeyNameModel { + + // MARK: Properties + + public let key: String + public let name: String + + // MARK: Initialisers + + init(_ payload: Components.Schemas.Tuple) { + self.key = payload.key + self.name = payload.name + } + +} diff --git a/Sources/Public/Models/AmiiboType.swift b/Sources/Public/Models/AmiiboType.swift new file mode 100644 index 0000000..2c57ee6 --- /dev/null +++ b/Sources/Public/Models/AmiiboType.swift @@ -0,0 +1,27 @@ +//===----------------------------------------------------------------------=== +// +// This source file is part of the AmiiboAPI open source project +// +// Copyright (c) 2024 Röck+Cöde VoF. and the AmiiboAPI project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of AmiiboAPI project authors +// +//===----------------------------------------------------------------------=== + +public struct AmiiboType: KeyNameModel { + + // MARK: Properties + + public let key: String + public let name: String + + // MARK: Initialisers + + init(_ payload: Components.Schemas.Tuple) { + self.key = payload.key + self.name = payload.name + } + +} diff --git a/Sources/Public/Models/GameCharacter.swift b/Sources/Public/Models/GameCharacter.swift new file mode 100644 index 0000000..2421104 --- /dev/null +++ b/Sources/Public/Models/GameCharacter.swift @@ -0,0 +1,27 @@ +//===----------------------------------------------------------------------=== +// +// This source file is part of the AmiiboAPI open source project +// +// Copyright (c) 2024 Röck+Cöde VoF. and the AmiiboAPI project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of AmiiboAPI project authors +// +//===----------------------------------------------------------------------=== + +public struct GameCharacter: KeyNameModel { + + // MARK: Properties + + public let key: String + public let name: String + + // MARK: Initialisers + + init(_ payload: Components.Schemas.Tuple) { + self.key = payload.key + self.name = payload.name + } + +} diff --git a/Sources/Public/Models/GameSeries.swift b/Sources/Public/Models/GameSeries.swift new file mode 100644 index 0000000..25d0ec8 --- /dev/null +++ b/Sources/Public/Models/GameSeries.swift @@ -0,0 +1,27 @@ +//===----------------------------------------------------------------------=== +// +// This source file is part of the AmiiboAPI open source project +// +// Copyright (c) 2024 Röck+Cöde VoF. and the AmiiboAPI project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of AmiiboAPI project authors +// +//===----------------------------------------------------------------------=== + +public struct GameSeries: KeyNameModel { + + // MARK: Properties + + public let key: String + public let name: String + + // MARK: Initialisers + + init(_ payload: Components.Schemas.Tuple) { + self.key = payload.key + self.name = payload.name + } + +} diff --git a/Sources/Public/Services/AmiiboService.swift b/Sources/Public/Services/AmiiboService.swift new file mode 100644 index 0000000..f983a3a --- /dev/null +++ b/Sources/Public/Services/AmiiboService.swift @@ -0,0 +1,63 @@ +//===----------------------------------------------------------------------=== +// +// This source file is part of the AmiiboAPI open source project +// +// Copyright (c) 2024 Röck+Cöde VoF. and the AmiiboAPI project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of AmiiboAPI project authors +// +//===----------------------------------------------------------------------=== + +import Foundation + +public struct AmiiboService { + + // MARK: Properties + + private let client: any APIClient + + // MARK: Initialisers + + public init(_ client: any APIClient) { + self.client = client + } + + // MARK: Functions + + public func getAmiibos( + _ filter: AmiiboFilter = .init() + ) async throws -> [Amiibo] { + try await client.getAmiibos(by: filter) + } + + public func getAmiiboSeries( + _ filter: AmiiboSeriesFilter = .init() + ) async throws -> [AmiiboSeries] { + try await client.getAmiiboSeries(by: filter) + } + + public func getAmiiboTypes( + _ filter: AmiiboTypeFilter = .init() + ) async throws -> [AmiiboType] { + try await client.getAmiiboTypes(by: filter) + } + + public func getGameCharacters( + _ filter: GameCharacterFilter = .init() + ) async throws -> [GameCharacter] { + try await client.getGameCharacters(by: filter) + } + + public func getGameSeries( + _ filter: GameSeriesFilter = .init() + ) async throws -> [GameSeries] { + try await client.getGameSeries(by: filter) + } + + public func getLastUpdated() async throws -> Date { + try await client.getLastUpdated() + } + +} diff --git a/Sources/amiibo_api.swift b/Sources/amiibo_api.swift deleted file mode 100644 index 08b22b8..0000000 --- a/Sources/amiibo_api.swift +++ /dev/null @@ -1,2 +0,0 @@ -// The Swift Programming Language -// https://docs.swift.org/swift-book diff --git a/Sources/openapi-generator-config.yaml b/Sources/openapi-generator-config.yaml new file mode 100644 index 0000000..5babde7 --- /dev/null +++ b/Sources/openapi-generator-config.yaml @@ -0,0 +1,16 @@ +# ===----------------------------------------------------------------------=== +# +# This source file is part of the AmiiboAPI open source project +# +# Copyright (c) 2024 Röck+Cöde VoF. and the AmiiboAPI project authors +# Licensed under the EUPL 1.2 or later. +# +# See LICENSE for license information +# See CONTRIBUTORS for the list of AmiiboAPI project authors +# +# ===----------------------------------------------------------------------=== + +generate: + - types + - client +accessModifier: internal diff --git a/Sources/openapi.yaml b/Sources/openapi.yaml new file mode 100644 index 0000000..b69ca5d --- /dev/null +++ b/Sources/openapi.yaml @@ -0,0 +1,376 @@ +# ===----------------------------------------------------------------------=== +# +# This source file is part of the AmiiboAPI open source project +# +# Copyright (c) 2024 Röck+Cöde VoF. and the AmiiboAPI project authors +# Licensed under the EUPL 1.2 or later. +# +# See LICENSE for license information +# See CONTRIBUTORS for the list of AmiiboAPI project authors +# +# ===----------------------------------------------------------------------=== + +openapi: '3.1.0' +info: + title: Amiibo API service + description: The Amiibo API RESTful service. + version: 1.0.0 +servers: + - url: https://www.amiiboapi.com/api + description: Amiibo API service (live) +paths: + /amiibo: + get: + description: Get a list of all the Amiibo items available in the database. + operationId: getAmiibos + responses: + '200': + description: Successful response returning the object that contains a list of Amiibo items. + content: + application/json: + schema: + $ref: '#/components/schemas/AmiiboWrapper' + '400': + description: Bad Amiibo request. + parameters: + - name: amiiboSeries + in: query + description: The Amiibo series identifier or name to filter the response. + required: false + schema: + type: string + style: form + - name: character + in: query + description: The game character identifier or name to filter the response. + required: false + schema: + type: string + style: form + - name: gameseries + in: query + description: The game series identifier or name to filter the response. + required: false + schema: + type: string + style: form + - name: id + in: query + description: The Amiibo identifier to filter the response. + required: false + schema: + type: string + style: form + - name: name + in: query + description: The Amiibo name to filter the response. + required: false + schema: + type: string + style: form + - name: showgames + in: query + description: The flag that indicates whether to include information about related games. + required: false + schema: + type: boolean + style: form + - name: showusage + in: query + description: The flag that indicates whether to include information about Amiibo usage in related games. + required: false + schema: + type: boolean + style: form + - name: type + in: query + description: The Amiibo type to filter the response. + required: false + schema: + type: string + style: form + + /amiiboseries: + get: + description: Get a list of all the Amiibo series available in the database. + operationId: getAmiiboSeries + responses: + '200': + description: Successful response returning the object that contains a list of Amiibo series. + content: + application/json: + schema: + $ref: '#/components/schemas/TupleWrapper' + '400': + description: Bad Amiibo series request. + '404': + description: Amiibo series not found. + '500': + description: Service currently not available. + parameters: + - name: key + in: query + description: The Amiibo series key to filter the response. + required: false + schema: + type: string + style: form + - name: name + in: query + description: The Amiibo series name to filter the response. + required: false + schema: + type: string + style: form + + /character: + get: + description: Get a list of all the game characters available in the database. + operationId: getGameCharacters + responses: + '200': + description: Successful response returning the object that contains a list of game characters. + content: + application/json: + schema: + $ref: '#/components/schemas/TupleWrapper' + '400': + description: Bad game character request. + '404': + description: Game character not found. + '500': + description: Service currently not available. + parameters: + - name: key + in: query + description: The game character key to filter the response. + required: false + schema: + type: string + style: form + - name: name + in: query + description: The game character name to filter the response. + required: false + schema: + type: string + style: form + + /gameseries: + get: + description: Gets a list of all the game series available in the database. + operationId: getGameSeries + responses: + '200': + description: Successful response returning the object that contains a list of game series. + content: + application/json: + schema: + $ref: '#/components/schemas/TupleWrapper' + '400': + description: Bad game series request. + '404': + description: Game series not found. + '500': + description: Service currently not available. + parameters: + - name: key + in: query + description: The game series key to filter the response. + required: false + schema: + type: string + style: form + - name: name + in: query + description: The game series name to filter the response. + required: false + schema: + type: string + style: form + + /type: + get: + description: Gets a list of all the Amiibo types available in the database. + operationId: getAmiiboTypes + responses: + '200': + description: Successful response returning the object that contains a list of Amiibo types. + content: + application/json: + schema: + $ref: '#/components/schemas/TupleWrapper' + '400': + description: Bad Amiibo type request. + '404': + description: Amiibo type not found. + '500': + description: Service currently not available. + parameters: + - name: key + in: query + description: The Amiibo type key to filter the response. + required: false + schema: + type: string + style: form + - name: name + in: query + description: The Amiibo type name to filter the response. + required: false + schema: + type: string + style: form + + /lastupdated: + get: + description: Gets a timestamp when the Amiibo data was last updated. + operationId: getLastUpdated + responses: + '200': + description: Successful response returning the object that contains the date and time when the database was last updated. + content: + application/json: + schema: + $ref: '#/components/schemas/LastUpdated' + +components: + schemas: + Amiibo: + type: object + properties: + amiiboSeries: + type: string + character: + type: string + gameSeries: + type: string + games3DS: + type: array + items: + $ref: '#/components/schemas/AmiiboGame' + gamesSwitch: + type: array + items: + $ref: '#/components/schemas/AmiiboGame' + gamesWiiU: + type: array + items: + $ref: '#/components/schemas/AmiiboGame' + head: + type: string + image: + type: string + name: + type: string + release: + type: object + $ref: '#/components/schemas/AmiiboRelease' + tail: + type: string + type: + type: string + required: + - amiiboSeries + - character + - gameSeries + - head + - image + - name + - release + - tail + - type + + AmiiboGame: + type: object + properties: + amiiboUsage: + type: array + items: + $ref: '#/components/schemas/AmiiboUsage' + gameID: + type: array + items: + type: string + gameName: + type: string + required: + - gameID + - gameName + + AmiiboList: + type: array + items: + $ref: '#/components/schemas/Amiibo' + + AmiiboRelease: + type: object + properties: + au: + type: string + format: date-time + eu: + type: string + format: date-time + jp: + type: string + format: date-time + na: + type: string + format: date-time + + AmiiboUsage: + type: object + properties: + Usage: + type: string + write: + type: boolean + required: + - Usage + - write + + AmiiboWrapper: + type: object + properties: + amiibo: + oneOf: + - $ref: '#/components/schemas/Amiibo' + - $ref: '#/components/schemas/AmiiboList' + required: + - amiibo + + LastUpdated: + type: object + properties: + lastUpdated: + type: string + format: date-time + required: + - lastUpdated + + Tuple: + type: object + properties: + key: + type: string + name: + type: string + required: + - key + - name + + TupleList: + type: array + items: + $ref: '#/components/schemas/Tuple' + + TupleWrapper: + type: object + properties: + amiibo: + oneOf: + - $ref: '#/components/schemas/Tuple' + - $ref: '#/components/schemas/TupleList' + required: + - amiibo diff --git a/Tests/AmiiboServiceLiveTests.swift b/Tests/AmiiboServiceLiveTests.swift new file mode 100644 index 0000000..6742af0 --- /dev/null +++ b/Tests/AmiiboServiceLiveTests.swift @@ -0,0 +1,1135 @@ +//===----------------------------------------------------------------------=== +// +// This source file is part of the AmiiboAPI open source project +// +// Copyright (c) 2024 Röck+Cöde VoF. and the AmiiboAPI project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of AmiiboAPI project authors +// +//===----------------------------------------------------------------------=== + +import AmiiboAPI +import Foundation +import Testing + +struct AmiiboServiceLiveTests { + + // MARK: Properties + + private let service: AmiiboService + + // MARK: Initialisers + + init() throws { + let client = try AmiiboLiveClient() + + self.service = .init(client) + } + + // MARK: Functions tests + + @Test("Get Amiibo items") + func getAmiibos() async throws { + // GIVEN + // WHEN + let amiibos = try await service.getAmiibos() + + // THEN + #expect(!amiibos.isEmpty) + #expect(amiibos.count == 853) + #expect(amiibos.first?.identifier == "0000000000000002") + #expect(amiibos.first?.platform == nil) + #expect(amiibos.last?.identifier == "3f000000042e0002") + #expect(amiibos.last?.platform == nil) + } + + @Test("Get Amiibo items by an existing identifier") + func getAmiibos_byExistingIdentifier() async throws { + // GIVEN + let identifier = "0000000000000002" + + // WHEN + let amiibos = try await service.getAmiibos(.init(identifier: identifier)) + + // THEN + #expect(!amiibos.isEmpty) + #expect(amiibos.count == 1) + #expect(amiibos.first?.identifier == identifier) + #expect(amiibos.first?.platform == nil) + } + + @Test("Get Amiibo items by a non-existing identifier") + func getAmiibos_byNonExistingIdentifier() async throws { + // GIVEN + let identifier = "0000000000000000" + + // WHEN + // THEN + await #expect(throws: AmiiboServiceError.decoding) { + try await service.getAmiibos(.init(identifier: identifier)) + } + } + + @Test("Get Amiibo items by an incomplete identifier") + func getAmiibos_byIncompleteIdentifier() async throws { + // GIVEN + let identifier = "0000000" + + // WHEN + // THEN + await #expect(throws: AmiiboServiceError.decoding) { + try await service.getAmiibos(.init(identifier: identifier)) + } + } + + @Test("Get Amiibo items by an empty identifier") + func getAmiibos_byEmptyIdentifier() async throws { + // GIVEN + let identifier = "" + + // WHEN + // THEN + await #expect(throws: AmiiboServiceError.badRequest) { + try await service.getAmiibos(.init(identifier: identifier)) + } + } + + @Test("Get Amiibo items by an existing name") + func getAmiibos_byExistingName() async throws { + // GIVEN + let name = "zelda" + + // WHEN + let amiibos = try await service.getAmiibos(.init(name: name)) + + // THEN + #expect(!amiibos.isEmpty) + #expect(amiibos.count == 5) + #expect(amiibos.first?.platform == nil) + #expect(amiibos.last?.platform == nil) + + let nameFirst = try #require(amiibos.first?.name.lowercased()) + let nameLast = try #require(amiibos.last?.name.lowercased()) + + #expect(nameFirst.contains(name)) + #expect(nameLast.contains(name)) + } + + @Test("Get Amiibo items by a non-existing name") + func getAmiibos_byNonExistingName() async throws { + // GIVEN + let name = "Something" + + // WHEN + let amiibos = try await service.getAmiibos(.init(name: name)) + + // THEN + #expect(amiibos.isEmpty) + } + + @Test("Get Amiibo items by an incomplete name") + func getAmiibos_byIncompleteName() async throws { + // GIVEN + let name = "zel" + + // WHEN + let amiibos = try await service.getAmiibos(.init(name: name)) + + // THEN + #expect(!amiibos.isEmpty) + #expect(amiibos.count == 7) + #expect(amiibos.first?.platform == nil) + #expect(amiibos.last?.platform == nil) + + let nameFirst = try #require(amiibos.first?.name.lowercased()) + let nameLast = try #require(amiibos.last?.name.lowercased()) + + #expect(nameFirst.contains(name)) + #expect(nameLast.contains(name)) + } + + @Test("Get Amiibo items by an empty name") + func getAmiibos_byEmptyName() async throws { + // GIVEN + let name = "" + + // WHEN + let amiibos = try await service.getAmiibos(.init(name: name)) + + // THEN + #expect(!amiibos.isEmpty) + #expect(amiibos.count == 853) + } + + @Test("Get Amiibo items by an existing type key") + func getAmiibos_byExistingTypeKey() async throws { + // GIVEN + let key = "0x00" + + // WHEN + let amiibos = try await service.getAmiibos(.init(type: key)) + + // THEN + #expect(!amiibos.isEmpty) + #expect(amiibos.count == 227) + #expect(amiibos.first?.type == "Figure") + #expect(amiibos.first?.platform == nil) + #expect(amiibos.last?.type == "Figure") + #expect(amiibos.last?.platform == nil) + } + + @Test("Get Amiibo items by an existing type name") + func getAmiibos_byExistingTypeName() async throws { + // GIVEN + let name = "figure" + + // WHEN + let amiibos = try await service.getAmiibos(.init(type: name)) + + // THEN + #expect(!amiibos.isEmpty) + #expect(amiibos.count == 227) + #expect(amiibos.first?.type == "Figure") + #expect(amiibos.first?.platform == nil) + #expect(amiibos.last?.type == "Figure") + #expect(amiibos.last?.platform == nil) + } + + @Test("Get Amiibo items by a non-existing type key") + func getAmiibos_byNonExistingTypeKey() async throws { + // GIVEN + let key = "0x0f" + + // WHEN + let amiibos = try await service.getAmiibos(.init(type: key)) + + // THEN + #expect(amiibos.isEmpty) + } + + @Test("Get Amiibo items by a non-existing type name") + func getAmiibos_byNonExistingTypeName() async throws { + // GIVEN + let name = "something" + + // WHEN + let amiibos = try await service.getAmiibos(.init(type: name)) + + // THEN + #expect(amiibos.isEmpty) + } + + @Test("Get Amiibo items by an incomplete type key") + func getAmiibos_byIncompleteTypeKey() async throws { + // GIVEN + let key = "0x" + + // WHEN + // THEN + await #expect(throws: AmiiboServiceError.badRequest) { + try await service.getAmiibos(.init(type: key)) + } + } + + @Test("Get Amiibo items by an incomplete type name") + func getAmiibos_byIncompleteTypeName() async throws { + // GIVEN + let name = "fig" + + // WHEN + let amiibos = try await service.getAmiibos(.init(type: name)) + + // THEN + #expect(amiibos.isEmpty) + } + + @Test("Get Amiibo items by an empty type key") + func getAmiibos_byEmptyTypeKey() async throws { + // GIVEN + let key = "" + + // WHEN + let amiibos = try await service.getAmiibos(.init(type: key)) + + // THEN + #expect(amiibos.isEmpty) + } + + @Test("Get Amiibo items by an empty type name") + func getAmiibos_byEmptyTypeName() async throws { + // GIVEN + let name = "" + + // WHEN + let amiibos = try await service.getAmiibos(.init(type: name)) + + // THEN + #expect(amiibos.isEmpty) + } + + @Test("Get Amiibo items by an existing series key") + func getAmiibos_byExistingSeriesKey() async throws { + // GIVEN + let key = "0x00" + + // WHEN + let amiibos = try await service.getAmiibos(.init(series: key)) + + // THEN + #expect(!amiibos.isEmpty) + #expect(amiibos.count == 96) + #expect(amiibos.first?.series == "Super Smash Bros.") + #expect(amiibos.first?.platform == nil) + #expect(amiibos.last?.series == "Super Smash Bros.") + #expect(amiibos.last?.platform == nil) + } + + @Test("Get Amiibo items by an existing series name") + func getAmiibos_byExistingSeriesName() async throws { + // GIVEN + let name = "Legend Of Zelda" + + // WHEN + let amiibos = try await service.getAmiibos(.init(series: name)) + + // THEN + #expect(!amiibos.isEmpty) + #expect(amiibos.count == 22) + #expect(amiibos.first?.series == name) + #expect(amiibos.first?.platform == nil) + #expect(amiibos.last?.series == name) + #expect(amiibos.last?.platform == nil) + } + + @Test("Get Amiibo items by a non-existing series key") + func getAmiibos_byNonExistingSeriesKey() async throws { + // GIVEN + let key = "0xf9" + + // WHEN + let amiibos = try await service.getAmiibos(.init(series: key)) + + // THEN + #expect(amiibos.isEmpty) + } + + @Test("Get Amiibo items by a non-existing series name") + func getAmiibos_byNonExistingSeriesName() async throws { + // GIVEN + let name = "something" + + // WHEN + let amiibos = try await service.getAmiibos(.init(series: name)) + + // THEN + #expect(amiibos.isEmpty) + } + + @Test("Get Amiibo items by an incomplete series key") + func getAmiibos_byIncompleteSeriesKey() async throws { + // GIVEN + let key = "0x" + + // WHEN + // THEN + await #expect(throws: AmiiboServiceError.badRequest) { + try await service.getAmiibos(.init(series: key)) + } + } + + @Test("Get Amiibo items by an incomplete series name") + func getAmiibos_byIncompleteSeriesName() async throws { + // GIVEN + let name = "fig" + + // WHEN + let amiibos = try await service.getAmiibos(.init(series: name)) + + // THEN + #expect(amiibos.isEmpty) + } + + @Test("Get Amiibo items by an empty series key") + func getAmiibos_byEmptySeriesKey() async throws { + // GIVEN + let key = "" + + // WHEN + let amiibos = try await service.getAmiibos(.init(series: key)) + + // THEN + #expect(!amiibos.isEmpty) + #expect(amiibos.count == 853) + } + + @Test("Get Amiibo items by an empty series name") + func getAmiibos_byEmptySeriesName() async throws { + // GIVEN + let name = "" + + // WHEN + let amiibos = try await service.getAmiibos(.init(series: name)) + + // THEN + #expect(!amiibos.isEmpty) + #expect(amiibos.count == 853) + } + + @Test("Get Amiibo items by an existing game character key") + func getAmiibos_byExistingGameCharacterKey() async throws { + // GIVEN + let key = "0x00" + + // WHEN + let amiibos = try await service.getAmiibos(.init(gameCharacter: key)) + + // THEN + #expect(!amiibos.isEmpty) + #expect(amiibos.count == 11) + #expect(amiibos.first?.gameCharacter == "Mario") + #expect(amiibos.first?.platform == nil) + #expect(amiibos.last?.gameCharacter == "Mario") + #expect(amiibos.last?.platform == nil) + } + + @Test("Get Amiibo items by an existing game character name") + func getAmiibos_byExistingGameCharacterName() async throws { + // GIVEN + let name = "Zelda" + + // WHEN + let amiibos = try await service.getAmiibos(.init(gameCharacter: name)) + + // THEN + #expect(!amiibos.isEmpty) + #expect(amiibos.count == 6) + #expect(amiibos.first?.gameCharacter == name) + #expect(amiibos.first?.platform == nil) + #expect(amiibos.last?.gameCharacter == name) + #expect(amiibos.last?.platform == nil) + } + + @Test("Get Amiibo items by a non-existing game character key") + func getAmiibos_byNonExistingGameCharacterKey() async throws { + // GIVEN + let key = "0xf9" + + // WHEN + let amiibos = try await service.getAmiibos(.init(gameCharacter: key)) + + // THEN + #expect(amiibos.isEmpty) + } + + @Test("Get Amiibo items by a non-existing game character name") + func getAmiibos_byNonExistingGameCharacterName() async throws { + // GIVEN + let name = "something" + + // WHEN + let amiibos = try await service.getAmiibos(.init(gameCharacter: name)) + + // THEN + #expect(amiibos.isEmpty) + } + + @Test("Get Amiibo items by an incomplete game character key") + func getAmiibos_byIncompleteGameCharacterKey() async throws { + // GIVEN + let key = "0x" + + // WHEN + // THEN + await #expect(throws: AmiiboServiceError.badRequest) { + try await service.getAmiibos(.init(gameCharacter: key)) + } + } + + @Test("Get Amiibo items by an incomplete game character name") + func getAmiibos_byIncompleteGameCharacterName() async throws { + // GIVEN + let name = "fig" + + // WHEN + let amiibos = try await service.getAmiibos(.init(gameCharacter: name)) + + // THEN + #expect(amiibos.isEmpty) + } + + @Test("Get Amiibo items by an empty game character key") + func getAmiibos_byEmptyGameCharacterKey() async throws { + // GIVEN + let key = "" + + // WHEN + let amiibos = try await service.getAmiibos(.init(gameCharacter: key)) + + // THEN + #expect(!amiibos.isEmpty) + #expect(amiibos.count == 853) + } + + @Test("Get Amiibo items by an empty game character name") + func getAmiibos_byEmptyGameCharacterName() async throws { + // GIVEN + let name = "" + + // WHEN + let amiibos = try await service.getAmiibos(.init(gameCharacter: name)) + + // THEN + #expect(!amiibos.isEmpty) + #expect(amiibos.count == 853) + } + + @Test("Get Amiibo items by an existing game series key") + func getAmiibos_byExistingGameSeriesKey() async throws { + // GIVEN + let key = "0x00" + + // WHEN + let amiibos = try await service.getAmiibos(.init(gameSeries: key)) + + // THEN + #expect(!amiibos.isEmpty) + #expect(amiibos.count == 42) + #expect(amiibos.first?.gameSeries == "Super Mario") + #expect(amiibos.first?.platform == nil) + #expect(amiibos.last?.gameSeries == "Super Mario") + #expect(amiibos.last?.platform == nil) + } + + @Test("Get Amiibo items by an existing game series name") + func getAmiibos_byExistingGameSeriesName() async throws { + // GIVEN + let name = "The Legend of Zelda" + + // WHEN + let amiibos = try await service.getAmiibos(.init(gameSeries: name)) + + // THEN + #expect(!amiibos.isEmpty) + #expect(amiibos.count == 28) + #expect(amiibos.first?.gameSeries == name) + #expect(amiibos.first?.platform == nil) + #expect(amiibos.last?.gameSeries == name) + #expect(amiibos.last?.platform == nil) + } + + @Test("Get Amiibo items by a non-existing game series key") + func getAmiibos_byNonExistingGameSeriesKey() async throws { + // GIVEN + let key = "0xf9" + + // WHEN + let amiibos = try await service.getAmiibos(.init(gameSeries: key)) + + // THEN + #expect(amiibos.isEmpty) + } + + @Test("Get Amiibo items by a non-existing game series name") + func getAmiibos_byNonExistingGameSeriesName() async throws { + // GIVEN + let name = "something" + + // WHEN + let amiibos = try await service.getAmiibos(.init(gameSeries: name)) + + // THEN + #expect(amiibos.isEmpty) + } + + @Test("Get Amiibo items by an incomplete game series key") + func getAmiibos_byIncompleteGameSeriesKey() async throws { + // GIVEN + let key = "0x" + + // WHEN + // THEN + await #expect(throws: AmiiboServiceError.badRequest) { + try await service.getAmiibos(.init(gameSeries: key)) + } + } + + @Test("Get Amiibo items by an incomplete game series name") + func getAmiibos_byIncompleteGameSeriesName() async throws { + // GIVEN + let name = "Super" + + // WHEN + let amiibos = try await service.getAmiibos(.init(gameSeries: name)) + + // THEN + #expect(!amiibos.isEmpty) + #expect(amiibos.count == 140) + } + + @Test("Get Amiibo items by an empty game series key") + func getAmiibos_byEmptyGameSeriesKey() async throws { + // GIVEN + let key = "" + + // WHEN + let amiibos = try await service.getAmiibos(.init(gameSeries: key)) + + // THEN + #expect(!amiibos.isEmpty) + #expect(amiibos.count == 853) + } + + @Test("Get Amiibo items by an empty game series name") + func getAmiibos_byEmptyGameSeriesName() async throws { + // GIVEN + let name = "" + + // WHEN + let amiibos = try await service.getAmiibos(.init(gameSeries: name)) + + // THEN + #expect(!amiibos.isEmpty) + #expect(amiibos.count == 853) + } + + @Test("Get Amiibo items with games data") + func getAmiibos_withGamesData() async throws { + // GIVEN + // WHEN + let amiibos = try await service.getAmiibos(.init(showGames: true)) + + // THEN + #expect(!amiibos.isEmpty) + #expect(amiibos.count == 853) + #expect(amiibos.first?.platform != nil) + #expect(amiibos.first?.platform?.switch.isEmpty == false) + #expect(amiibos.first?.platform?.switch.first?.usages == nil) + #expect(amiibos.first?.platform?.threeDS.isEmpty == false) + #expect(amiibos.first?.platform?.threeDS.first?.usages == nil) + #expect(amiibos.first?.platform?.wiiU.isEmpty == false) + #expect(amiibos.first?.platform?.wiiU.first?.usages == nil) + #expect(amiibos.last?.platform != nil) + } + + @Test("Get Amiibo items with games and usages data") + func getAmiibos_withGamesAndUsagesData() async throws { + // GIVEN + // WHEN + let amiibos = try await service.getAmiibos(.init(showUsage: true)) + + // THEN + #expect(!amiibos.isEmpty) + #expect(amiibos.count == 853) + #expect(amiibos.first?.platform != nil) + #expect(amiibos.first?.platform?.switch.isEmpty == false) + #expect(amiibos.first?.platform?.switch.first?.usages?.isEmpty == false) + #expect(amiibos.first?.platform?.threeDS.isEmpty == false) + #expect(amiibos.first?.platform?.threeDS.first?.usages?.isEmpty == false) + #expect(amiibos.first?.platform?.wiiU.isEmpty == false) + #expect(amiibos.first?.platform?.wiiU.first?.usages?.isEmpty == false) + #expect(amiibos.last?.platform != nil) + } + + @Test("Get Amiibo series") + func getAmiiboSeries() async throws { + // GIVEN + // WHEN + let amiiboSeries = try await service.getAmiiboSeries() + + // THEN + #expect(!amiiboSeries.isEmpty) + #expect(amiiboSeries.count == 26) + #expect(amiiboSeries.first?.key == "0x00") + #expect(amiiboSeries.last?.key == "0xff") + } + + @Test("Get Amiibo series by an existing key") + func getAmiiboSeries_byExistingKey() async throws { + // GIVEN + let key = "0x01" + + // WHEN + let amiiboSeries = try await service.getAmiiboSeries(.init(key: key)) + + // THEN + #expect(!amiiboSeries.isEmpty) + #expect(amiiboSeries.count == 1) + #expect(amiiboSeries.first?.key == key) + } + + @Test("Get Amiibo series by a non-existing key") + func getAmiiboSeries_byNonExistingKey() async throws { + // GIVEN + let key = "0xf9" + + // WHEN + // THEN + await #expect(throws: AmiiboServiceError.notFound) { + try await service.getAmiiboSeries(.init(key: key)) + } + } + + @Test("Get Amiibo series by an incomplete key") + func getAmiiboSeries_byIncompleteKey() async throws { + // GIVEN + let key = "0x" + + // WHEN + // THEN + await #expect(throws: AmiiboServiceError.badRequest) { + try await service.getAmiiboSeries(.init(key: key)) + } + } + + @Test("Get Amiibo series by an empty key") + func getAmiiboSeries_byEmptyKey() async throws { + // GIVEN + let key = "" + + // WHEN + // THEN + await #expect(throws: AmiiboServiceError.badRequest) { + try await service.getAmiiboSeries(.init(key: key)) + } + } + + @Test("Get Amiibo series by an existing name") + func getAmiiboSeries_byExistingName() async throws { + // GIVEN + let name = "Legend Of Zelda" + + // WHEN + let amiiboSeries = try await service.getAmiiboSeries(.init(name: name)) + + // THEN + #expect(!amiiboSeries.isEmpty) + #expect(amiiboSeries.count == 1) + #expect(amiiboSeries.first?.name == name) + } + + @Test("Get Amiibo series by a non-existing name") + func getAmiiboSeries_byNonExistingName() async throws { + // GIVEN + let name = "Something" + + // WHEN + // THEN + await #expect(throws: AmiiboServiceError.notFound) { + try await service.getAmiiboSeries(.init(name: name)) + } + } + + @Test("Get Amiibo series by an incomplete name") + func getAmiiboSeries_byIncompleteName() async throws { + // GIVEN + let name = "Zelda" + + // WHEN + let amiiboSeries = try await service.getAmiiboSeries(.init(name: name)) + + // THEN + #expect(!amiiboSeries.isEmpty) + #expect(amiiboSeries.count == 1) + + let amiiboSeriesName = try #require(amiiboSeries.first) + + #expect(amiiboSeriesName.name.contains(name)) + } + + @Test("Get Amiibo series by an empty name") + func getAmiiboSeries_byEmptyName() async throws { + // GIVEN + let name = "" + + // WHEN + let amiiboSeries = try await service.getAmiiboSeries(.init(name: name)) + + // THEN + #expect(!amiiboSeries.isEmpty) + #expect(amiiboSeries.count == 26) + #expect(amiiboSeries.first?.key == "0x00") + #expect(amiiboSeries.last?.key == "0xff") + } + + @Test("Get Amiibo types") + func getAmiiboTypes() async throws { + // GIVEN + // WHEN + let amiiboTypes = try await service.getAmiiboTypes() + + // THEN + #expect(!amiiboTypes.isEmpty) + #expect(amiiboTypes.count == 4) + #expect(amiiboTypes.first?.key == "0x00") + #expect(amiiboTypes.last?.key == "0x03") + } + + @Test("Get Amiibo types by an existing key") + func getAmiiboTypes_byExistingKey() async throws { + // GIVEN + let key = "0x01" + + // WHEN + let amiiboTypes = try await service.getAmiiboTypes(.init(key: key)) + + // THEN + #expect(!amiiboTypes.isEmpty) + #expect(amiiboTypes.count == 1) + #expect(amiiboTypes.first?.key == key) + } + + @Test("Get Amiibo types by a non-existing key") + func getAmiiboTypes_byNonExistingKey() async throws { + // GIVEN + let key = "0x09" + + // WHEN + // THEN + await #expect(throws: AmiiboServiceError.notFound) { + try await service.getAmiiboTypes(.init(key: key)) + } + } + + @Test("Get Amiibo types by an incomplete key") + func getAmiiboTypes_byIncompleteKey() async throws { + // GIVEN + let key = "0x" + + // WHEN + // THEN + await #expect(throws: AmiiboServiceError.badRequest) { + try await service.getAmiiboTypes(.init(key: key)) + } + } + + @Test("Get Amiibo types by an empty key") + func getAmiiboTypes_byEmptyKey() async throws { + // GIVEN + let key = "" + + // WHEN + // THEN + await #expect(throws: AmiiboServiceError.badRequest) { + try await service.getAmiiboTypes(.init(key: key)) + } + } + + @Test("Get Amiibo types by an existing name") + func getAmiiboTypes_byExistingName() async throws { + // GIVEN + let name = "Card" + + // WHEN + let amiiboTypes = try await service.getAmiiboTypes(.init(name: name)) + + // THEN + #expect(!amiiboTypes.isEmpty) + #expect(amiiboTypes.count == 1) + #expect(amiiboTypes.first?.name == name) + } + + @Test("Get Amiibo types by a non-existing name") + func getAmiiboTypes_byNonExistingName() async throws { + // GIVEN + let name = "Something" + + // WHEN + // THEN + await #expect(throws: AmiiboServiceError.notFound) { + try await service.getAmiiboTypes(.init(name: name)) + } + } + + @Test("Get Amiibo types by an incomplete name") + func getAmiiboTypes_byIncompleteName() async throws { + // GIVEN + let name = "Ca" + + // WHEN + let amiiboTypes = try await service.getAmiiboTypes(.init(name: name)) + + // THEN + #expect(!amiiboTypes.isEmpty) + #expect(amiiboTypes.count == 1) + + let amiiboTypeName = try #require(amiiboTypes.first) + + #expect(amiiboTypeName.name.contains(name)) + } + + @Test("Get Amiibo types by an empty name") + func getAmiiboTypes_byEmptyName() async throws { + // GIVEN + let name = "" + + // WHEN + let amiiboTypes = try await service.getAmiiboTypes(.init(name: name)) + + // THEN + #expect(!amiiboTypes.isEmpty) + #expect(amiiboTypes.count == 4) + #expect(amiiboTypes.first?.key == "0x00") + #expect(amiiboTypes.last?.key == "0x03") + } + + @Test("Get game characters") + func getGameCharacters() async throws { + // GIVEN + // WHEN + let gameCharacters = try await service.getGameCharacters() + + // THEN + #expect(!gameCharacters.isEmpty) + #expect(gameCharacters.count == 644) + #expect(gameCharacters.first?.key == "0x0000") + #expect(gameCharacters.last?.key == "0x3f00") + } + + @Test("Get game characters by an existing key") + func getGameCharacters_byExistingKey() async throws { + // GIVEN + let key = "0x0001" + + // WHEN + let gameCharacters = try await service.getGameCharacters(.init(key: key)) + + // THEN + #expect(!gameCharacters.isEmpty) + #expect(gameCharacters.count == 1) + #expect(gameCharacters.first?.key == key) + } + + @Test("Get game characters by a non-existing key") + func getGameCharacters_byNonExistingKey() async throws { + // GIVEN + let key = "0xffff" + + // WHEN + // THEN + await #expect(throws: AmiiboServiceError.notFound) { + try await service.getGameCharacters(.init(key: key)) + } + } + + @Test("Get game characters by an incomplete key") + func getGameCharacters_byIncompleteKey() async throws { + // GIVEN + let key = "0x" + + // WHEN + // THEN + await #expect(throws: AmiiboServiceError.badRequest) { + try await service.getGameCharacters(.init(key: key)) + } + } + + @Test("Get game characters by an empty key") + func getGameCharacters_byEmptyKey() async throws { + // GIVEN + let key = "" + + // WHEN + // THEN + await #expect(throws: AmiiboServiceError.badRequest) { + try await service.getGameCharacters(.init(key: key)) + } + } + + @Test("Get game characters by an existing name") + func getGameCharacters_byExistingName() async throws { + // GIVEN + let name = "Zelda" + + // WHEN + let gameCharacters = try await service.getGameCharacters(.init(name: name)) + + // THEN + #expect(!gameCharacters.isEmpty) + #expect(gameCharacters.count == 1) + #expect(gameCharacters.first?.name == name) + } + + @Test("Get game characters by a non-existing name") + func getGameCharacters_byNonExistingName() async throws { + // GIVEN + let name = "Something" + + // WHEN + // THEN + await #expect(throws: AmiiboServiceError.notFound) { + try await service.getGameCharacters(.init(name: name)) + } + } + + @Test("Get game characters by an incomplete name") + func getGameCharacters_byIncompleteName() async throws { + // GIVEN + let name = "Zeld" + + // WHEN + let gameCharacters = try await service.getGameCharacters(.init(name: name)) + + // THEN + #expect(!gameCharacters.isEmpty) + #expect(gameCharacters.count == 1) + + let gameCharactersName = try #require(gameCharacters.first) + + #expect(gameCharactersName.name.contains(name)) + } + + @Test("Get game characters by an empty name") + func getGameCharacters_byEmptyName() async throws { + // GIVEN + let name = "" + + // WHEN + let gameCharacters = try await service.getGameCharacters(.init(name: name)) + + // THEN + #expect(!gameCharacters.isEmpty) + #expect(gameCharacters.count == 644) + #expect(gameCharacters.first?.key == "0x0000") + #expect(gameCharacters.last?.key == "0x3f00") + } + + @Test("Get game series") + func getGameSeries() async throws { + // GIVEN + // WHEN + let gameSeries = try await service.getGameSeries() + + // THEN + #expect(!gameSeries.isEmpty) + #expect(gameSeries.count == 116) + #expect(gameSeries.first?.key == "0x000") + #expect(gameSeries.last?.key == "0x3f0") + } + + @Test("Get game series by an existing key") + func getGameSeries_byExistingKey() async throws { + // GIVEN + let key = "0x001" + + // WHEN + let gameSeries = try await service.getGameSeries(.init(key: key)) + + // THEN + #expect(!gameSeries.isEmpty) + #expect(gameSeries.count == 1) + #expect(gameSeries.first?.key == key) + } + + @Test("Get game series by a non-existing key") + func getGameSeries_byNonExistingKey() async throws { + // GIVEN + let key = "0xffff" + + // WHEN + // THEN + await #expect(throws: AmiiboServiceError.notFound) { + try await service.getGameSeries(.init(key: key)) + } + } + + @Test("Get game series by an incomplete key") + func getGameSeries_byIncompleteKey() async throws { + // GIVEN + let key = "0x" + + // WHEN & THEN + await #expect(throws: AmiiboServiceError.badRequest) { + try await service.getGameSeries(.init(key: key)) + } + } + + @Test("Get game series by an empty key") + func getGameSeries_byEmptyKey() async throws { + // GIVEN + let key = "" + + // WHEN + // THEN + await #expect(throws: AmiiboServiceError.badRequest) { + try await service.getGameSeries(.init(key: key)) + } + } + + @Test("Get game series by an existing name") + func getGameSeries_byExistingName() async throws { + // GIVEN + let name = "Pikmin" + + // WHEN + let gameSeries = try await service.getGameSeries(.init(name: name)) + + // THEN + #expect(!gameSeries.isEmpty) + #expect(gameSeries.count == 1) + #expect(gameSeries.first?.name == name) + } + + @Test("Get game series by a non-existing name") + func getGameSeries_byNonExistingName() async throws { + // GIVEN + let name = "Something" + + // WHEN + // THEN + await #expect(throws: AmiiboServiceError.notFound) { + try await service.getGameSeries(.init(name: name)) + } + } + + @Test("Get game series by an incomplete name") + func getGameSeries_byIncompleteName() async throws { + // GIVEN + let name = "Pik" + + // WHEN + let gameSeries = try await service.getGameSeries(.init(name: name)) + + // THEN + #expect(!gameSeries.isEmpty) + #expect(gameSeries.count == 1) + + let gameSeriesName = try #require(gameSeries.first) + + #expect(gameSeriesName.name.contains(name)) + } + + @Test("Get game series by an empty name") + func getGameSeries_byEmptyName() async throws { + // GIVEN + let name = "" + + // WHEN + let gameSeries = try await service.getGameSeries(.init(name: name)) + + // THEN + #expect(!gameSeries.isEmpty) + #expect(gameSeries.count == 116) + #expect(gameSeries.first?.key == "0x000") + #expect(gameSeries.last?.key == "0x3f0") + } + + @Test("Get the last updated timestamp") + func getLastUpdated() async throws { + // GIVEN + // WHEN + let dateLastUpdated = try await service.getLastUpdated() + + // THEN + let dateComponents = Calendar.current.dateComponents( + [.year, .month, .day], + from: dateLastUpdated + ) + + #expect(dateComponents.year == 2024) + #expect(dateComponents.month == 9) + #expect(dateComponents.day == 6) + } + +} diff --git a/Tests/amiibo_apiTests.swift b/Tests/amiibo_apiTests.swift deleted file mode 100644 index 26a09ea..0000000 --- a/Tests/amiibo_apiTests.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Testing - -@testable import AmiiboAPI - -@Test func example() async throws { - // Write your test here and use APIs like `#expect(...)` to check expected conditions. -}