DocC documentation support (#4)

This PR contains the work done to:
* Documented all the `private`, `internal`, and `public` interfaces on the existing codebase;
* Set the DocC documentation catalog in the project;
* Written the main `Library` article for the DocC documentation catalog;
* Added the documentation tasks in the `Makefile` file.

Reviewed-on: #4
Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
This commit was merged in pull request #4.
This commit is contained in:
2025-09-09 17:30:19 +00:00
committed by Javier Cicchelli
parent 39c6d6e8d6
commit 37c0f3e322
30 changed files with 543 additions and 151 deletions
+11
View File
@@ -0,0 +1,11 @@
# --- DOCUMENTATION ---
DOCC_CATALOG_PATH=./Catalogs/AmiiboService.docc
DOCC_GITHUB_OUTPUT=./docs
DOCC_GITHUB_BASE_PATH=amiibo-service
DOCC_PREVIEW_URL=http://localhost:8080/documentation/amiiboservice
DOCC_XCODE_OUTPUT=./${SPM_LIBRARY_TARGET}.doccarchive
# -- SWIFT PACKAGE MANAGER ---
SPM_LIBRARY_TARGET=AmiiboService
+4 -1
View File
@@ -31,4 +31,7 @@ Packages/
# hence it is not needed unless you have added a package configuration file to your project # hence it is not needed unless you have added a package configuration file to your project
.swiftpm .swiftpm
.swiftpm/configuration/registries.json .swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
# DocC documentation
*.doccarchive
+74
View File
@@ -0,0 +1,74 @@
# ``AmiiboService``
A library that provides everything the developer needs to interacts with the **Amiibo API** online service.
## Overview
The `AmiiboService` library is a Swift Package Manager package dependency aims at allowing the developer to interact with the [Amiibo API](https://www.amiiboapi.com) online service seamlessly, by not only providing the *service* tye but also any possible *clients*, *models*, *filters* and *errors* type that might be needed.
## Design
Although it could have been possible to generate a one-to-one RESTful client based on the Open API specification document that describe the available endpoints, it was decided to design a ``AmiiboService`` service that removes the complexities of the service's backend API, and provides the developer with a simple interface, and a seamless experience.
## Instalation
To use the `AmiiboService` library with your package, then add it as a dependency in the `Package.swift` file:
```swift
let package = Package(
// name, platforms, products, etc.
dependencies: [
.package(url: "https://github.com/rock-n-code/amiibo-service", from: "1.0.0"),
// other dependencies
],
targets: [
.target(
name: "SomeTarget",
dependencies: [
.product(name: "AmiiboService", package: "amiibo-service"),
]
)
// other targets
]
)
```
It is also possible to use the `AmiiboService` library with your app in Xcode, then add it as a dependency in your Xcode project:
> important: Swift 5.9 or higher is required in order to compile this library.
## Topics
### Service
- ``AmiiboService``
### Clients
- ``AmiiboClient``
- ``AmiiboLiveClient``
- ``AmiiboMockClient``
### Models
- ``Amiibo``
- ``Amiibo/Game``
- ``Amiibo/Platform``
- ``Amiibo/Release``
- ``Amiibo/Usage``
- ``AmiiboSeries``
- ``AmiiboType``
- ``GameCharacter``
- ``GameSeries``
### Filters
- ``AmiiboFilter``
- ``AmiiboSeriesFilter``
- ``AmiiboTypeFilter``
- ``GameCharacterFilter``
- ``GameSeriesFilter``
### Errors
- ``AmiiboServiceError``
+39 -8
View File
@@ -17,14 +17,6 @@ environment ?= .env
include $(environment) include $(environment)
export $(shell sed 's/=.*//' $(environment)) export $(shell sed 's/=.*//' $(environment))
# IDE
open-in-xcode: ## Opens this package with Xcode
@open -a Xcode Package.swift
open-in-vscode: ## Opens this package with Visual Studio Code
@code .
# SWIFT PACKAGE MANAGER # SWIFT PACKAGE MANAGER
package-build: ## Builds the project locally package-build: ## Builds the project locally
@@ -41,6 +33,45 @@ package-reset: ## Resets the complete SPM cache/build folder from the package
package-update: ## Updates the SPM package dependencies package-update: ## Updates the SPM package dependencies
@swift package update @swift package update
# DOCUMENTATION
doc-generate: doc-generate-xcode doc-generate-github ## Generates the library documentation for both Github and Xcode
doc-generate-github: ## Generates the library documentation for Github
@swift package \
--allow-writing-to-directory $(DOCC_GITHUB_OUTPUT) \
generate-documentation \
$(DOCC_CATALOG_PATH) \
--target $(SPM_LIBRARY_TARGET) \
--disable-indexing \
--transform-for-static-hosting \
--hosting-base-path $(DOCC_GITHUB_BASE_PATH) \
--output-path $(DOCC_GITHUB_OUTPUT)
doc-generate-xcode: ## Generates the library documentation for Xcode
@swift package \
--allow-writing-to-directory $(DOCC_XCODE_OUTPUT) \
generate-documentation \
$(DOCC_CATALOG_PATH) \
--target $(SPM_LIBRARY_TARGET) \
--output-path $(DOCC_XCODE_OUTPUT)
doc-preview: ## Previews the library documentation in Safari
@open -a safari $(DOCC_PREVIEW_URL)
@swift package \
--disable-sandbox \
preview-documentation \
$(DOCC_CATALOG_PATH) \
--target $(SPM_LIBRARY_TARGET)
# IDE
open-in-xcode: ## Opens this package with Xcode
@open -a Xcode Package.swift
open-in-vscode: ## Opens this package with Visual Studio Code
@code .
# HELP # HELP
+8 -27
View File
@@ -26,44 +26,25 @@ let package = Package(
products: [ products: [
.library( .library(
name: AmiiboService.package, name: AmiiboService.package,
targets: [ targets: [AmiiboService.target]
AmiiboService.target
]
) )
], ],
dependencies: [ dependencies: [
.package( .package(url: "https://github.com/apple/swift-openapi-generator.git", from: "1.3.0"),
url: "https://github.com/apple/swift-openapi-generator.git", .package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.5.0"),
from: "1.3.0" .package(url: "https://github.com/apple/swift-openapi-urlsession", from: "1.0.2"),
), .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.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: [ targets: [
.target( .target(
name: AmiiboService.target, name: AmiiboService.target,
dependencies: [ dependencies: [
.product( .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
name: "OpenAPIRuntime", .product(name: "OpenAPIURLSession", package: "swift-openapi-urlsession")
package: "swift-openapi-runtime"
),
.product(
name: "OpenAPIURLSession",
package: "swift-openapi-urlsession"
)
], ],
path: "Sources", path: "Sources",
plugins: [ plugins: [
.plugin( .plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator"),
name: "OpenAPIGenerator",
package: "swift-openapi-generator"
),
] ]
), ),
.testTarget( .testTarget(
@@ -14,7 +14,13 @@ import Foundation
extension DateFormatter { extension DateFormatter {
static var isoDateTime: DateFormatter { // MARK: Properties
/// An ISO timestamp formatter.
///
/// This formatter implements the `yyyy-MM-dd'T'HH:mm:ss.SSSSSS` custom date format.
/// Within the context of this library, this formatter is solely used to decode a date formatted as a timestamp that is returned by the ``AmiiboService/getLastUpdated()`` function.
static var isoTimestamp: DateFormatter {
let formatter = DateFormatter() let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS" formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS"
+36 -7
View File
@@ -12,15 +12,44 @@
import Foundation import Foundation
public protocol APIClient { /// A protocol that defines API clients containing all available endpoints to interact with.
protocol APIClient {
// MARK: Functions // MARK: Functions
func getAmiibos(by filter: AmiiboFilter) async throws -> [Amiibo] /// Gets a list of amiibo items based on a given filter.
func getAmiiboSeries(by filter: AmiiboSeriesFilter) async throws -> [AmiiboSeries] /// - Parameter filter: A filter to remove unwanted items from the result.
func getAmiiboTypes(by filter: AmiiboTypeFilter) async throws -> [AmiiboType] /// - Returns: A list of filtered amiibo items.
func getGameCharacters(by filter: GameCharacterFilter) async throws -> [GameCharacter] /// - Throws: An ``AmiiboServiceError`` error in case some issue is encountered while generating the result.
func getGameSeries(by filter: GameSeriesFilter) async throws -> [GameSeries] func getAmiibos(by filter: AmiiboFilter) async throws(AmiiboServiceError) -> [Amiibo]
func getLastUpdated() async throws -> Date
/// Gets a list of amiibo series based on a given filter.
/// - Parameter filter: A filter to remove unwanted items from the result.
/// - Returns: A list of filtered amiibo series.
/// - Throws: An ``AmiiboServiceError`` error in case some issue is encountered while generating the result.
func getAmiiboSeries(by filter: AmiiboSeriesFilter) async throws(AmiiboServiceError) -> [AmiiboSeries]
/// Gets a list of amiibo types based on a given filter.
/// - Parameter filter: A filter to remove unwanted items from the result.
/// - Returns: A list of filtered amiibo types.
/// - Throws: An ``AmiiboServiceError`` error in case some issue is encountered while generating the result.
func getAmiiboTypes(by filter: AmiiboTypeFilter) async throws(AmiiboServiceError) -> [AmiiboType]
/// Gets a list of game characters based on a given filter.
/// - Parameter filter: A filter to remove unwanted items from the result.
/// - Returns: A list of filtered game characters.
/// - Throws: An ``AmiiboServiceError`` error in case some issue is encountered while generating the result.
func getGameCharacters(by filter: GameCharacterFilter) async throws(AmiiboServiceError) -> [GameCharacter]
/// Gets a list of game series based on a given filter.
/// - Parameter filter: A filter to remove unwanted items from the result.
/// - Returns: A list of filtered game series.
/// - Throws: An ``AmiiboServiceError`` error in case some issue is encountered while generating the result.
func getGameSeries(by filter: GameSeriesFilter) async throws(AmiiboServiceError) -> [GameSeries]
/// Gets the date when the data was last updated.
/// - Returns: A last updated date.
/// - Throws: An ``AmiiboServiceError`` error in case some issue is encountered while generating the result.
func getLastUpdated() async throws(AmiiboServiceError) -> Date
} }
+12 -1
View File
@@ -10,17 +10,28 @@
// //
//===----------------------------------------------------------------------=== //===----------------------------------------------------------------------===
/// A protocol that defines filters that might contain `key` and/or `name` values.
protocol KeyNameFilter { protocol KeyNameFilter {
// MARK: Properties // MARK: Properties
/// A key to use for filtering, if any.
var key: String? { get } var key: String? { get }
/// A name to use for filtering, if any.
var name: String? { get } var name: String? { get }
// MARK: Initialisers // MARK: Initializers
/// Initializes this filter without key or name values.
init() init()
/// Initializes this filter with a key value.
/// - Parameter key: A key to use for filtering.
init(key: String) init(key: String)
/// Initializes this filter with a name value.
/// - Parameter name: A name to use for filtering.
init(name: String) init(name: String)
} }
@@ -10,15 +10,21 @@
// //
//===----------------------------------------------------------------------=== //===----------------------------------------------------------------------===
/// A protocol that defines decodable models containing the `key` and `name` properties.
protocol KeyNameModel: Sendable { protocol KeyNameModel: Sendable {
// MARK: Properties // MARK: Properties
/// A key.
var key: String { get } var key: String { get }
/// A name.
var name: String { get } var name: String { get }
// MARK: Initialisers // MARK: Initializers
/// Initializes this model from a given payload.
/// - Parameter payload: A payload that contains the values for the model.
init(_ payload: Components.Schemas.Tuple) init(_ payload: Components.Schemas.Tuple)
} }
@@ -13,10 +13,19 @@
import Foundation import Foundation
import OpenAPIRuntime import OpenAPIRuntime
struct ISODateTranscoder: DateTranscoder { /// A type that allows the decoding and encoding of ISO timestamp dates, defined by the `yyyy-MM-dd'T'HH:mm:ss.SSSSSS` custom date format.
struct ISOTimestampTranscoder {
// MARK: Properties // MARK: Properties
private let dateFormatter: DateFormatter = .isoDateTime
/// A formatter to use to decode and encode ISO timestamps dates.
private let dateFormatter: DateFormatter = .isoTimestamp
}
// MARK: - DateTranscoder
extension ISOTimestampTranscoder: DateTranscoder {
// MARK: Functions // MARK: Functions
+111 -57
View File
@@ -14,55 +14,61 @@ import Foundation
import OpenAPIRuntime import OpenAPIRuntime
import OpenAPIURLSession import OpenAPIURLSession
/// A type that implements a live client to the online service.
public struct AmiiboLiveClient { public struct AmiiboLiveClient {
// MARK: Properties // MARK: Properties
/// A client generated by the `OpenAPIRuntime` library.
private let client: Client private let client: Client
// MARK: Initialisers // MARK: Initializers
public init() throws { /// Initializes this client.
public init() {
self.client = .init( self.client = .init(
serverURL: try Servers.Server1.url(), // The force unwrapping implemented below assumes that the server definition from the OpenAPI specification is correct.
configuration: .init(dateTranscoder: ISODateTranscoder()), serverURL: try! Servers.Server1.url(),
configuration: .init(dateTranscoder: ISOTimestampTranscoder()),
transport: URLSessionTransport() transport: URLSessionTransport()
) )
} }
} }
// MARK: - APIProtocol // MARK: - APIClient
extension AmiiboLiveClient: APIClient { extension AmiiboLiveClient: APIClient {
// MARK: Functions // MARK: Functions
public func getAmiibos(by filter: AmiiboFilter) async throws -> [Amiibo] { public func getAmiibos(
let response = try await { by filter: AmiiboFilter
do { ) async throws(AmiiboServiceError) -> [Amiibo] {
return try await client.getAmiibos( let response: Operations.getAmiibos.Output
.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
}
do {
response = 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 {
if error.underlyingError is DecodingError {
throw AmiiboServiceError.decoding throw AmiiboServiceError.decoding
} catch { } else {
throw AmiiboServiceError.unknown throw AmiiboServiceError.unknown
} }
}() } catch {
throw AmiiboServiceError.unknown
}
switch response { switch response {
case let .ok(ok): case let .ok(ok):
@@ -79,13 +85,21 @@ extension AmiiboLiveClient: APIClient {
} }
} }
public func getAmiiboSeries(by filter: AmiiboSeriesFilter) async throws -> [AmiiboSeries] { public func getAmiiboSeries(
let response = try await client.getAmiiboSeries( by filter: AmiiboSeriesFilter
.init(query: .init( ) async throws(AmiiboServiceError) -> [AmiiboSeries] {
key: filter.key, let response: Operations.getAmiiboSeries.Output
name: filter.name
)) do {
) response = try await client.getAmiiboSeries(
.init(query: .init(
key: filter.key,
name: filter.name
))
)
} catch {
throw AmiiboServiceError.unknown
}
switch response { switch response {
case let .ok(ok): case let .ok(ok):
@@ -108,13 +122,21 @@ extension AmiiboLiveClient: APIClient {
} }
} }
public func getAmiiboTypes(by filter: AmiiboTypeFilter) async throws -> [AmiiboType] { public func getAmiiboTypes(
let response = try await client.getAmiiboTypes( by filter: AmiiboTypeFilter
.init(query: .init( ) async throws(AmiiboServiceError) -> [AmiiboType] {
key: filter.key, let response: Operations.getAmiiboTypes.Output
name: filter.name
)) do {
) response = try await client.getAmiiboTypes(
.init(query: .init(
key: filter.key,
name: filter.name
))
)
} catch {
throw AmiiboServiceError.unknown
}
switch response { switch response {
case let .ok(ok): case let .ok(ok):
@@ -137,13 +159,21 @@ extension AmiiboLiveClient: APIClient {
} }
} }
public func getGameCharacters(by filter: GameCharacterFilter) async throws -> [GameCharacter] { public func getGameCharacters(
let response = try await client.getGameCharacters( by filter: GameCharacterFilter
.init(query: .init( ) async throws(AmiiboServiceError) -> [GameCharacter] {
key: filter.key, let response: Operations.getGameCharacters.Output
name: filter.name
)) do {
) response = try await client.getGameCharacters(
.init(query: .init(
key: filter.key,
name: filter.name
))
)
} catch {
throw AmiiboServiceError.unknown
}
switch response { switch response {
case let .ok(ok): case let .ok(ok):
@@ -166,13 +196,21 @@ extension AmiiboLiveClient: APIClient {
} }
} }
public func getGameSeries(by filter: GameSeriesFilter) async throws -> [GameSeries] { public func getGameSeries(
let response = try await client.getGameSeries( by filter: GameSeriesFilter
.init(query: .init( ) async throws(AmiiboServiceError) -> [GameSeries] {
key: filter.key, let response: Operations.getGameSeries.Output
name: filter.name
)) do {
) response = try await client.getGameSeries(
.init(query: .init(
key: filter.key,
name: filter.name
))
)
} catch {
throw AmiiboServiceError.unknown
}
switch response { switch response {
case let .ok(ok): case let .ok(ok):
@@ -195,8 +233,14 @@ extension AmiiboLiveClient: APIClient {
} }
} }
public func getLastUpdated() async throws -> Date { public func getLastUpdated() async throws(AmiiboServiceError) -> Date {
let response = try await client.getLastUpdated() let response: Operations.getLastUpdated.Output
do {
response = try await client.getLastUpdated()
} catch {
throw AmiiboServiceError.unknown
}
switch response { switch response {
case let .ok(ok): case let .ok(ok):
@@ -218,7 +262,12 @@ private extension AmiiboLiveClient {
// MARK: Functions // MARK: Functions
func map(_ wrapper: Components.Schemas.AmiiboWrapper) -> [Amiibo] { /// Retrieves a list of amiibo items from a wrapper container.
/// - Parameter wrapper: A wrapper container that either has an object or a list of items.
/// - Returns: A list of amiibo items, sorted by identifiers.
func map(
_ wrapper: Components.Schemas.AmiiboWrapper
) -> [Amiibo] {
switch wrapper.amiibo { switch wrapper.amiibo {
case let .Amiibo(object): case let .Amiibo(object):
return [.init(object)] return [.init(object)]
@@ -230,6 +279,11 @@ private extension AmiiboLiveClient {
} }
} }
/// Retrieves a list of items that conforms to the `KeyNameModel` protocol from a wrapper container.
/// - Parameters:
/// - wrapper: A wrapper container that either has an object or a list of items.
/// - as: a model type that conforms to the `KeyNameModel` protocol.
/// - Returns: A list of items that conforms to the `KeyNameModel` protocol, sorted by keys.
func map<Model: KeyNameModel>( func map<Model: KeyNameModel>(
_ wrapper: Components.Schemas.TupleWrapper, _ wrapper: Components.Schemas.TupleWrapper,
as: Model.Type as: Model.Type
+33 -8
View File
@@ -12,20 +12,43 @@
import Foundation import Foundation
/// A type that implements a mock client, for testing purposes.
public struct AmiiboMockClient { public struct AmiiboMockClient {
// MARK: Properties // MARK: Properties
/// A list of amiibo items to return, if any.
private let amiibos: [Amiibo]? private let amiibos: [Amiibo]?
/// A list of amiibo series to return, if any.
private let amiiboSeries: [AmiiboSeries]? private let amiiboSeries: [AmiiboSeries]?
/// A list of amiibo types to return, if any.
private let amiiboTypes: [AmiiboType]? private let amiiboTypes: [AmiiboType]?
/// An error to throw, if any.
private let error: AmiiboServiceError? private let error: AmiiboServiceError?
/// A list of game characters to return, if any.
private let gameCharacters: [GameCharacter]? private let gameCharacters: [GameCharacter]?
/// A list of game series to return, if any.
private let gameSeries: [GameSeries]? private let gameSeries: [GameSeries]?
/// A last updated date to return, if any.
private let lastUpdated: Date? private let lastUpdated: Date?
// MARK: Initialisers // MARK: Initializers
/// Initializes this client.
/// - Parameters:
/// - amiibos: A list of amiibo items to return, if any.
/// - amiiboSeries: A list of amiibo series to return, if any.
/// - amiiboTypes: A list of amiibo types to return, if any.
/// - gameCharacters: A list of game characters to return, if any.
/// - gameSeries: A list of game series to return, if any.
/// - lastUpdated: A last updated date to return, if any.
/// - error: An error to throw, if any.
public init( public init(
amiibos: [Amiibo]? = nil, amiibos: [Amiibo]? = nil,
amiiboSeries: [AmiiboSeries]? = nil, amiiboSeries: [AmiiboSeries]? = nil,
@@ -52,7 +75,7 @@ extension AmiiboMockClient: APIClient {
// MARK: Functions // MARK: Functions
public func getAmiibos(by filter: AmiiboFilter) async throws -> [Amiibo] { public func getAmiibos(by filter: AmiiboFilter) async throws(AmiiboServiceError) -> [Amiibo] {
try throwErrorIfExists() try throwErrorIfExists()
guard let amiibos else { guard let amiibos else {
@@ -62,7 +85,7 @@ extension AmiiboMockClient: APIClient {
return amiibos return amiibos
} }
public func getAmiiboSeries(by filter: AmiiboSeriesFilter) async throws -> [AmiiboSeries] { public func getAmiiboSeries(by filter: AmiiboSeriesFilter) async throws(AmiiboServiceError) -> [AmiiboSeries] {
try throwErrorIfExists() try throwErrorIfExists()
guard let amiiboSeries else { guard let amiiboSeries else {
@@ -72,7 +95,7 @@ extension AmiiboMockClient: APIClient {
return amiiboSeries return amiiboSeries
} }
public func getAmiiboTypes(by filter: AmiiboTypeFilter) async throws -> [AmiiboType] { public func getAmiiboTypes(by filter: AmiiboTypeFilter) async throws(AmiiboServiceError) -> [AmiiboType] {
try throwErrorIfExists() try throwErrorIfExists()
guard let amiiboTypes else { guard let amiiboTypes else {
@@ -82,7 +105,7 @@ extension AmiiboMockClient: APIClient {
return amiiboTypes return amiiboTypes
} }
public func getGameCharacters(by filter: GameCharacterFilter) async throws -> [GameCharacter] { public func getGameCharacters(by filter: GameCharacterFilter) async throws(AmiiboServiceError) -> [GameCharacter] {
try throwErrorIfExists() try throwErrorIfExists()
guard let gameCharacters else { guard let gameCharacters else {
@@ -92,7 +115,7 @@ extension AmiiboMockClient: APIClient {
return gameCharacters return gameCharacters
} }
public func getGameSeries(by filter: GameSeriesFilter) async throws -> [GameSeries] { public func getGameSeries(by filter: GameSeriesFilter) async throws(AmiiboServiceError) -> [GameSeries] {
try throwErrorIfExists() try throwErrorIfExists()
guard let gameSeries else { guard let gameSeries else {
@@ -102,7 +125,7 @@ extension AmiiboMockClient: APIClient {
return gameSeries return gameSeries
} }
public func getLastUpdated() async throws -> Date { public func getLastUpdated() async throws(AmiiboServiceError) -> Date {
try throwErrorIfExists() try throwErrorIfExists()
guard let lastUpdated else { guard let lastUpdated else {
@@ -121,7 +144,9 @@ private extension AmiiboMockClient {
// MARK: Functions // MARK: Functions
func throwErrorIfExists() throws { /// Throws an error if it has been provided,
/// - Throws: An ``AmiiboServiceError`` error in case an error has been provided.
func throwErrorIfExists() throws(AmiiboServiceError) {
if let error { if let error {
throw error throw error
} }
@@ -0,0 +1,21 @@
//===----------------------------------------------------------------------===
//
// This source file is part of the AmiiboService open source project
//
// Copyright (c) 2024-2025 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
//
//===----------------------------------------------------------------------===
/// A concrete representation of the types of client that a ``AmiiboService`` service can utilize.
///
/// > important: This enumeration has been defined as a way to avoid exposing the `APIClient` protocol outside the boundaries of this library.
public enum AmiiboClient {
/// A live Amiibo client to interact with the online service.
case live(AmiiboLiveClient = .init())
///A mock Amiibo client, for testing purposes.
case mock(AmiiboMockClient)
}
@@ -10,12 +10,19 @@
// //
//===----------------------------------------------------------------------=== //===----------------------------------------------------------------------===
/// A representation of all the possible errors that the ``AmiiboService`` service could throw.
public enum AmiiboServiceError: Error { public enum AmiiboServiceError: Error {
/// A bad request has been given to the client.
case badRequest case badRequest
/// A response cannot be decoded.
case decoding case decoding
/// An online service is not currently available.
case notAvailable case notAvailable
/// A response cannot be found.
case notFound case notFound
/// An undocumented/unsupported status code error.
case undocumented(_ statusCode: Int) case undocumented(_ statusCode: Int)
/// An unknown error.
case unknown case unknown
} }
+27 -1
View File
@@ -10,21 +10,47 @@
// //
//===----------------------------------------------------------------------=== //===----------------------------------------------------------------------===
/// A type that contains values to fine-tune a response when requesting amiibo items.
public struct AmiiboFilter { public struct AmiiboFilter {
// MARK: Properties // MARK: Properties
/// A game character to return, if any.
public let gameCharacter: String? public let gameCharacter: String?
/// A game series to return, if any.
public let gameSeries: String? public let gameSeries: String?
/// An amiibo identifier to return, if any.
public let identifier: String? public let identifier: String?
/// An amiibo name to return, if any.
public let name: String? public let name: String?
/// An amiibo series to return, if any.
public let series: String? public let series: String?
/// A flag indicating whether to include games in the response, if any.
public let showGames: Bool? public let showGames: Bool?
/// A flag indicating whether to include amiibo usages in games in the response, if any.
public let showUsage: Bool? public let showUsage: Bool?
/// An amiibo type to return, if any.
public let type: String? public let type: String?
// MARK: Initialisers // MARK: Initializers
/// Initializes this filter.
/// - Parameters:
/// - identifier: An amiibo identifier to return, if any.
/// - name: An amiibo name to return, if any.
/// - type: An amiibo type to return, if any.
/// - series: An amiibo series to return, if any.
/// - gameCharacter: A game character to return, if any.
/// - gameSeries: A game series to return, if any.
/// - showGames: A flag indicating whether to include games in the response, if any.
/// - showUsage: A flag indicating whether to include amiibo usages in games in the response, if any.
public init( public init(
identifier: String? = nil, identifier: String? = nil,
name: String? = nil, name: String? = nil,
@@ -10,6 +10,7 @@
// //
//===----------------------------------------------------------------------=== //===----------------------------------------------------------------------===
/// A type that contains values to fine-tune a response when requesting amiibo series.
public struct AmiiboSeriesFilter: KeyNameFilter { public struct AmiiboSeriesFilter: KeyNameFilter {
// MARK: Properties // MARK: Properties
@@ -17,7 +18,7 @@ public struct AmiiboSeriesFilter: KeyNameFilter {
public let key: String? public let key: String?
public let name: String? public let name: String?
// MARK: Initialisers // MARK: Initializers
public init() { public init() {
self.key = nil self.key = nil
@@ -10,6 +10,7 @@
// //
//===----------------------------------------------------------------------=== //===----------------------------------------------------------------------===
/// A type that contains values to fine-tune a response when requesting amiibo types.
public struct AmiiboTypeFilter: KeyNameFilter { public struct AmiiboTypeFilter: KeyNameFilter {
// MARK: Properties // MARK: Properties
@@ -17,7 +18,7 @@ public struct AmiiboTypeFilter: KeyNameFilter {
public let key: String? public let key: String?
public let name: String? public let name: String?
// MARK: Initialisers // MARK: Initializers
public init() { public init() {
self.key = nil self.key = nil
@@ -10,6 +10,7 @@
// //
//===----------------------------------------------------------------------=== //===----------------------------------------------------------------------===
/// A type that contains values to fine-tune a response when requesting game characters.
public struct GameCharacterFilter: KeyNameFilter { public struct GameCharacterFilter: KeyNameFilter {
// MARK: Properties // MARK: Properties
@@ -17,7 +18,7 @@ public struct GameCharacterFilter: KeyNameFilter {
public let key: String? public let key: String?
public let name: String? public let name: String?
// MARK: Initialisers // MARK: Initializers
public init() { public init() {
self.key = nil self.key = nil
@@ -10,6 +10,7 @@
// //
//===----------------------------------------------------------------------=== //===----------------------------------------------------------------------===
/// A type that contains values to fine-tune a response when requesting game series.
public struct GameSeriesFilter: KeyNameFilter { public struct GameSeriesFilter: KeyNameFilter {
// MARK: Properties // MARK: Properties
@@ -17,7 +18,7 @@ public struct GameSeriesFilter: KeyNameFilter {
public let key: String? public let key: String?
public let name: String? public let name: String?
// MARK: Initialisers // MARK: Initializers
public init() { public init() {
self.key = nil self.key = nil
+26 -2
View File
@@ -12,23 +12,45 @@
import Foundation import Foundation
/// A model that represents an amiibo item.
public struct Amiibo: Sendable { public struct Amiibo: Sendable {
// MARK: Properties // MARK: Properties
/// A game character.
public let gameCharacter: String public let gameCharacter: String
/// A game series.
public let gameSeries: String public let gameSeries: String
/// The first 8 hexadecimal characters of an identifier.
public let head: String public let head: String
/// An image link.
public let image: String public let image: String
/// An amiibo name.
public let name: String public let name: String
/// A game platform type, if any.
public let platform: Platform? public let platform: Platform?
/// A release date.
public let release: Release public let release: Release
/// An amiibo series.
public let series: String public let series: String
/// The last 8 hexadecimal characters of an identifier.
public let tail: String public let tail: String
/// An amiibo type.
public let type: String public let type: String
// MARK: Initialisers // MARK: Initializers
/// Initializes this model from a given payload.
/// - Parameter payload: A payload that contains the values for the model.
init(_ payload: Components.Schemas.Amiibo) { init(_ payload: Components.Schemas.Amiibo) {
self.gameCharacter = payload.character self.gameCharacter = payload.character
self.gameSeries = payload.gameSeries self.gameSeries = payload.gameSeries
@@ -48,10 +70,12 @@ public struct Amiibo: Sendable {
// MARK: Computed // MARK: Computed
/// An identifier.
public var identifier: String { public var identifier: String {
head + tail head + tail
} }
/// A URL related to an image link, if any.
public var imageURL: URL? { public var imageURL: URL? {
.init(string: image) .init(string: image)
} }
+10 -2
View File
@@ -11,16 +11,24 @@
//===----------------------------------------------------------------------=== //===----------------------------------------------------------------------===
extension Amiibo { extension Amiibo {
/// A model that represents a game related to an amiibo item.
public struct Game: Sendable { public struct Game: Sendable {
// MARK: Properties // MARK: Properties
/// A list of identifiers.
public let identifiers: [String] public let identifiers: [String]
/// A name.
public let name: String public let name: String
/// A list of amiibo usages, if any.
public let usages: [Usage]? public let usages: [Usage]?
// MARK: Initialisers // MARK: Initializers
/// Initializes this model from a given payload.
/// - Parameter payload: A payload that contains the values for the model.
init(_ payload: Components.Schemas.AmiiboGame) { init(_ payload: Components.Schemas.AmiiboGame) {
self.identifiers = payload.gameID self.identifiers = payload.gameID
self.name = payload.gameName self.name = payload.gameName
@@ -11,16 +11,30 @@
//===----------------------------------------------------------------------=== //===----------------------------------------------------------------------===
extension Amiibo { extension Amiibo {
/// A model that represents a collection of `WiiU`, `3DS`, and `Switch` games related to an amiibo item.
public struct Platform: Sendable { public struct Platform: Sendable {
// MARK: Properties // MARK: Properties
/// A list of `Switch` games related to an amiibo item.
public let `switch`: [Game] public let `switch`: [Game]
/// A list of `3DS` games related to an amiibo item.
public let threeDS: [Game] public let threeDS: [Game]
/// A list of `WiiU` games related to an amiibo item.
public let wiiU: [Game] public let wiiU: [Game]
// MARK: Initialisers // MARK: Initialisers
/// Initializes this model.
///
/// > important: In case no data is provided, then an instance of this model is not created.
///
/// - Parameters:
/// - `switch`: A list of `Switch` games related to an amiibo item, if any.
/// - threeDS: A list of `3DS` games related to an amiibo item, if any.
/// - wiiU: A list of `WiiU` games related to an amiibo item, if any.
init?( init?(
_ `switch`: [Components.Schemas.AmiiboGame]?, _ `switch`: [Components.Schemas.AmiiboGame]?,
_ threeDS: [Components.Schemas.AmiiboGame]?, _ threeDS: [Components.Schemas.AmiiboGame]?,
@@ -13,17 +13,27 @@
import Foundation import Foundation
extension Amiibo { extension Amiibo {
/// A model that represents a collection of release dates related to an amiibo item.
public struct Release: Sendable { public struct Release: Sendable {
// MARK: Properties // MARK: Properties
/// A release date for North America, if any.
public let america: Date? public let america: Date?
/// A release date for Australia, if any.
public let australia: Date? public let australia: Date?
/// A release date for Europe, if any.
public let europe: Date? public let europe: Date?
/// A release date for Japan, if any.
public let japan: Date? public let japan: Date?
// MARK: Initialisers // MARK: Initializers
/// Initializes this model from a given payload.
/// - Parameter payload: A payload that contains the values for the model.
init(_ payload: Components.Schemas.AmiiboRelease) { init(_ payload: Components.Schemas.AmiiboRelease) {
self.america = payload.na self.america = payload.na
self.australia = payload.au self.australia = payload.au
@@ -11,15 +11,21 @@
//===----------------------------------------------------------------------=== //===----------------------------------------------------------------------===
extension Amiibo { extension Amiibo {
/// A model that represents the usage of an amiibo item within a certain game.
public struct Usage: Sendable { public struct Usage: Sendable {
// MARK: Properties // MARK: Properties
/// An explanation of how to use an amiibo item.
public let explanation: String public let explanation: String
/// A flag that indicates whether an amiibo item can save game data in it.
public let isWriteable: Bool public let isWriteable: Bool
// MARK: Initialisers // MARK: Initializers
/// Initializes this model from a given payload.
/// - Parameter payload: A payload that contains the values for the model.
init(_ payload: Components.Schemas.AmiiboUsage) { init(_ payload: Components.Schemas.AmiiboUsage) {
self.explanation = payload.Usage self.explanation = payload.Usage
self.isWriteable = payload.write self.isWriteable = payload.write
+2 -1
View File
@@ -10,6 +10,7 @@
// //
//===----------------------------------------------------------------------=== //===----------------------------------------------------------------------===
/// A model that represents an amiibo series.
public struct AmiiboSeries: KeyNameModel { public struct AmiiboSeries: KeyNameModel {
// MARK: Properties // MARK: Properties
@@ -17,7 +18,7 @@ public struct AmiiboSeries: KeyNameModel {
public let key: String public let key: String
public let name: String public let name: String
// MARK: Initialisers // MARK: Initializers
init(_ payload: Components.Schemas.Tuple) { init(_ payload: Components.Schemas.Tuple) {
self.key = payload.key self.key = payload.key
+2 -1
View File
@@ -10,6 +10,7 @@
// //
//===----------------------------------------------------------------------=== //===----------------------------------------------------------------------===
/// A model that represents an amiibo type.
public struct AmiiboType: KeyNameModel { public struct AmiiboType: KeyNameModel {
// MARK: Properties // MARK: Properties
@@ -17,7 +18,7 @@ public struct AmiiboType: KeyNameModel {
public let key: String public let key: String
public let name: String public let name: String
// MARK: Initialisers // MARK: Initializers
init(_ payload: Components.Schemas.Tuple) { init(_ payload: Components.Schemas.Tuple) {
self.key = payload.key self.key = payload.key
+2 -1
View File
@@ -10,6 +10,7 @@
// //
//===----------------------------------------------------------------------=== //===----------------------------------------------------------------------===
/// A model that represents a game character.
public struct GameCharacter: KeyNameModel { public struct GameCharacter: KeyNameModel {
// MARK: Properties // MARK: Properties
@@ -17,7 +18,7 @@ public struct GameCharacter: KeyNameModel {
public let key: String public let key: String
public let name: String public let name: String
// MARK: Initialisers // MARK: Initializers
init(_ payload: Components.Schemas.Tuple) { init(_ payload: Components.Schemas.Tuple) {
self.key = payload.key self.key = payload.key
+2 -1
View File
@@ -10,6 +10,7 @@
// //
//===----------------------------------------------------------------------=== //===----------------------------------------------------------------------===
/// A model that represents a game series.
public struct GameSeries: KeyNameModel { public struct GameSeries: KeyNameModel {
// MARK: Properties // MARK: Properties
@@ -17,7 +18,7 @@ public struct GameSeries: KeyNameModel {
public let key: String public let key: String
public let name: String public let name: String
// MARK: Initialisers // MARK: Initializers
init(_ payload: Components.Schemas.Tuple) { init(_ payload: Components.Schemas.Tuple) {
self.key = payload.key self.key = payload.key
+46 -16
View File
@@ -12,51 +12,81 @@
import Foundation import Foundation
/// A type that implements the service that uses a client to make calls.
public struct AmiiboService { public struct AmiiboService {
// MARK: Properties // MARK: Properties
/// A client to interact with the endpoints.
private let client: any APIClient private let client: any APIClient
// MARK: Initialisers // MARK: Initializers
public init(_ client: any APIClient) { /// Initializes this service with a specific client type.
self.client = client /// - Parameter client: A representation of a client to use to interact with the endpoints.
public init(_ client: AmiiboClient) {
self.client = switch client {
case let .mock(mockClient): mockClient
case let .live(liveClient): liveClient
}
} }
// MARK: Functions // MARK: Functions
/// Gets a list of amiibo items based on a given filter.
/// - Parameter filter: A filter to remove unwanted items from the result.
/// - Returns: A list of filtered amiibo items.
/// - Throws: An ``AmiiboServiceError`` error in case some issue is encountered while generating the result.
public func getAmiibos( public func getAmiibos(
_ filter: AmiiboFilter = .init() _ filter: AmiiboFilter = .init()
) async throws -> [Amiibo] { ) async throws(AmiiboServiceError) -> [Amiibo] {
try await client.getAmiibos(by: filter) try await client.getAmiibos(by: filter)
} }
/// Gets a list of amiibo series based on a given filter.
/// - Parameter filter: A filter to remove unwanted items from the result.
/// - Returns: A list of filtered amiibo series.
/// - Throws: An ``AmiiboServiceError`` error in case some issue is encountered while generating the result.
public func getAmiiboSeries( public func getAmiiboSeries(
_ filter: AmiiboSeriesFilter = .init() _ filter: AmiiboSeriesFilter = .init()
) async throws -> [AmiiboSeries] { ) async throws(AmiiboServiceError) -> [AmiiboSeries] {
try await client.getAmiiboSeries(by: filter) try await client.getAmiiboSeries(by: filter)
} }
/// Gets a list of amiibo types based on a given filter.
/// - Parameter filter: A filter to remove unwanted items from the result.
/// - Returns: A list of filtered amiibo types.
/// - Throws: An ``AmiiboServiceError`` error in case some issue is encountered while generating the result.
public func getAmiiboTypes( public func getAmiiboTypes(
_ filter: AmiiboTypeFilter = .init() _ filter: AmiiboTypeFilter = .init()
) async throws -> [AmiiboType] { ) async throws(AmiiboServiceError) -> [AmiiboType] {
try await client.getAmiiboTypes(by: filter) try await client.getAmiiboTypes(by: filter)
} }
/// Gets a list of game characters based on a given filter.
/// - Parameter filter: A filter to remove unwanted items from the result.
/// - Returns: A list of filtered game characters.
/// - Throws: An ``AmiiboServiceError`` error in case some issue is encountered while generating the result.
public func getGameCharacters( public func getGameCharacters(
_ filter: GameCharacterFilter = .init() _ filter: GameCharacterFilter = .init()
) async throws -> [GameCharacter] { ) async throws(AmiiboServiceError) -> [GameCharacter] {
try await client.getGameCharacters(by: filter) try await client.getGameCharacters(by: filter)
} }
/// Gets a list of game series based on a given filter.
/// - Parameter filter: A filter to remove unwanted items from the result.
/// - Returns: A list of filtered game series.
/// - Throws: An ``AmiiboServiceError`` error in case some issue is encountered while generating the result.
public func getGameSeries( public func getGameSeries(
_ filter: GameSeriesFilter = .init() _ filter: GameSeriesFilter = .init()
) async throws -> [GameSeries] { ) async throws(AmiiboServiceError) -> [GameSeries] {
try await client.getGameSeries(by: filter) try await client.getGameSeries(by: filter)
} }
public func getLastUpdated() async throws -> Date { /// Gets the date when the data was last updated.
/// - Returns: A last updated date.
/// - Throws: An ``AmiiboServiceError`` error in case some issue is encountered while generating the result.
public func getLastUpdated() async throws(AmiiboServiceError) -> Date {
try await client.getLastUpdated() try await client.getLastUpdated()
} }
@@ -23,10 +23,8 @@ struct AmiiboServiceLiveTests {
// MARK: Initialisers // MARK: Initialisers
init() throws { init() {
let client = try AmiiboLiveClient() self.service = .init(.live())
self.service = .init(client)
} }
// MARK: Functions tests // MARK: Functions tests