From 37c0f3e3227ad3d8f0b1faecf9631bd2f8b8257c Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 9 Sep 2025 17:30:19 +0000 Subject: [PATCH] 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: https://repo.rock-n-code.com/rock-n-code/amiibo-service/pulls/4 Co-authored-by: Javier Cicchelli Co-committed-by: Javier Cicchelli --- .env | 11 ++ .gitignore | 5 +- Catalogs/AmiiboService.docc/Library.md | 74 ++++++++ Makefile | 47 ++++- Package.swift | 35 +--- .../Extensions/DateFormatter+Properties.swift | 8 +- Sources/Internal/Protocols/APIClient.swift | 43 ++++- .../Internal/Protocols/KeyNameFilter.swift | 13 +- Sources/Internal/Protocols/KeyNameModel.swift | 8 +- ...der.swift => ISOTimestampTranscoder.swift} | 15 +- Sources/Public/Clients/AmiiboLiveClient.swift | 168 ++++++++++++------ Sources/Public/Clients/AmiiboMockClient.swift | 41 ++++- .../Public/Enumerations/AmiiboClient.swift | 21 +++ .../Public/Errors/AmiiboServiceError.swift | 7 + Sources/Public/Filters/AmiiboFilter.swift | 28 ++- .../Public/Filters/AmiiboSeriesFilter.swift | 3 +- Sources/Public/Filters/AmiiboTypeFilter.swift | 3 +- .../Public/Filters/GameCharacterFilter.swift | 3 +- Sources/Public/Filters/GameSeriesFilter.swift | 3 +- Sources/Public/Models/Amiibo.swift | 28 ++- .../Public/Models/Amiibo/Amiibo+Game.swift | 12 +- .../Models/Amiibo/Amiibo+Platform.swift | 14 ++ .../Public/Models/Amiibo/Amiibo+Release.swift | 14 +- .../Public/Models/Amiibo/Amiibo+Usage.swift | 10 +- Sources/Public/Models/AmiiboSeries.swift | 3 +- Sources/Public/Models/AmiiboType.swift | 3 +- Sources/Public/Models/GameCharacter.swift | 3 +- Sources/Public/Models/GameSeries.swift | 3 +- Sources/Public/Services/AmiiboService.swift | 62 +++++-- .../Services}/AmiiboServiceLiveTests.swift | 6 +- 30 files changed, 543 insertions(+), 151 deletions(-) create mode 100644 Catalogs/AmiiboService.docc/Library.md rename Sources/Internal/Transcoders/{ISODateTranscoder.swift => ISOTimestampTranscoder.swift} (66%) create mode 100644 Sources/Public/Enumerations/AmiiboClient.swift rename Tests/{ => Public/Services}/AmiiboServiceLiveTests.swift (99%) diff --git a/.env b/.env index e69de29..65a9167 100644 --- a/.env +++ b/.env @@ -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 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3c13662..7f85c9f 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,7 @@ Packages/ # hence it is not needed unless you have added a package configuration file to your project .swiftpm .swiftpm/configuration/registries.json -.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata \ No newline at end of file +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata + +# DocC documentation +*.doccarchive \ No newline at end of file diff --git a/Catalogs/AmiiboService.docc/Library.md b/Catalogs/AmiiboService.docc/Library.md new file mode 100644 index 0000000..5c8c692 --- /dev/null +++ b/Catalogs/AmiiboService.docc/Library.md @@ -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`` diff --git a/Makefile b/Makefile index 04010b0..46195d0 100644 --- a/Makefile +++ b/Makefile @@ -17,14 +17,6 @@ environment ?= .env include $(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 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 @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 diff --git a/Package.swift b/Package.swift index 041d9d2..16057bd 100644 --- a/Package.swift +++ b/Package.swift @@ -26,44 +26,25 @@ let package = Package( products: [ .library( name: AmiiboService.package, - targets: [ - AmiiboService.target - ] + targets: [AmiiboService.target] ) ], 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" - ) + .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"), + .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0"), ], targets: [ .target( name: AmiiboService.target, dependencies: [ - .product( - name: "OpenAPIRuntime", - package: "swift-openapi-runtime" - ), - .product( - name: "OpenAPIURLSession", - package: "swift-openapi-urlsession" - ) + .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"), + .product(name: "OpenAPIURLSession", package: "swift-openapi-urlsession") ], path: "Sources", plugins: [ - .plugin( - name: "OpenAPIGenerator", - package: "swift-openapi-generator" - ), + .plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator"), ] ), .testTarget( diff --git a/Sources/Internal/Extensions/DateFormatter+Properties.swift b/Sources/Internal/Extensions/DateFormatter+Properties.swift index 5a8e372..69818bf 100644 --- a/Sources/Internal/Extensions/DateFormatter+Properties.swift +++ b/Sources/Internal/Extensions/DateFormatter+Properties.swift @@ -14,7 +14,13 @@ import Foundation 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() formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS" diff --git a/Sources/Internal/Protocols/APIClient.swift b/Sources/Internal/Protocols/APIClient.swift index bcb90ae..c018758 100644 --- a/Sources/Internal/Protocols/APIClient.swift +++ b/Sources/Internal/Protocols/APIClient.swift @@ -12,15 +12,44 @@ import Foundation -public protocol APIClient { +/// A protocol that defines API clients containing all available endpoints to interact with. +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 + /// 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. + func getAmiibos(by filter: AmiiboFilter) async throws(AmiiboServiceError) -> [Amiibo] + + /// 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 } diff --git a/Sources/Internal/Protocols/KeyNameFilter.swift b/Sources/Internal/Protocols/KeyNameFilter.swift index ad7d825..a57cc74 100644 --- a/Sources/Internal/Protocols/KeyNameFilter.swift +++ b/Sources/Internal/Protocols/KeyNameFilter.swift @@ -10,17 +10,28 @@ // //===----------------------------------------------------------------------=== +/// A protocol that defines filters that might contain `key` and/or `name` values. protocol KeyNameFilter { // MARK: Properties + /// A key to use for filtering, if any. var key: String? { get } + + /// A name to use for filtering, if any. var name: String? { get } - // MARK: Initialisers + // MARK: Initializers + /// Initializes this filter without key or name values. init() + + /// Initializes this filter with a key value. + /// - Parameter key: A key to use for filtering. init(key: String) + + /// Initializes this filter with a name value. + /// - Parameter name: A name to use for filtering. init(name: String) } diff --git a/Sources/Internal/Protocols/KeyNameModel.swift b/Sources/Internal/Protocols/KeyNameModel.swift index 0d5bc16..64c0d73 100644 --- a/Sources/Internal/Protocols/KeyNameModel.swift +++ b/Sources/Internal/Protocols/KeyNameModel.swift @@ -10,15 +10,21 @@ // //===----------------------------------------------------------------------=== +/// A protocol that defines decodable models containing the `key` and `name` properties. protocol KeyNameModel: Sendable { // MARK: Properties + /// A key. var key: String { get } + + /// A name. 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) } diff --git a/Sources/Internal/Transcoders/ISODateTranscoder.swift b/Sources/Internal/Transcoders/ISOTimestampTranscoder.swift similarity index 66% rename from Sources/Internal/Transcoders/ISODateTranscoder.swift rename to Sources/Internal/Transcoders/ISOTimestampTranscoder.swift index 6b81d5b..90eb7c2 100644 --- a/Sources/Internal/Transcoders/ISODateTranscoder.swift +++ b/Sources/Internal/Transcoders/ISOTimestampTranscoder.swift @@ -13,10 +13,19 @@ import Foundation 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 - 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 diff --git a/Sources/Public/Clients/AmiiboLiveClient.swift b/Sources/Public/Clients/AmiiboLiveClient.swift index 49ef078..d087300 100644 --- a/Sources/Public/Clients/AmiiboLiveClient.swift +++ b/Sources/Public/Clients/AmiiboLiveClient.swift @@ -14,55 +14,61 @@ import Foundation import OpenAPIRuntime import OpenAPIURLSession +/// A type that implements a live client to the online service. public struct AmiiboLiveClient { // MARK: Properties + /// A client generated by the `OpenAPIRuntime` library. private let client: Client - // MARK: Initialisers + // MARK: Initializers - public init() throws { + /// Initializes this client. + public init() { self.client = .init( - serverURL: try Servers.Server1.url(), - configuration: .init(dateTranscoder: ISODateTranscoder()), + // The force unwrapping implemented below assumes that the server definition from the OpenAPI specification is correct. + serverURL: try! Servers.Server1.url(), + configuration: .init(dateTranscoder: ISOTimestampTranscoder()), transport: URLSessionTransport() ) } } -// MARK: - APIProtocol +// MARK: - APIClient 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 - } + public func getAmiibos( + by filter: AmiiboFilter + ) async throws(AmiiboServiceError) -> [Amiibo] { + let response: Operations.getAmiibos.Output + 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 - } catch { + } else { throw AmiiboServiceError.unknown } - }() + } catch { + throw AmiiboServiceError.unknown + } switch response { case let .ok(ok): @@ -79,13 +85,21 @@ extension AmiiboLiveClient: APIClient { } } - public func getAmiiboSeries(by filter: AmiiboSeriesFilter) async throws -> [AmiiboSeries] { - let response = try await client.getAmiiboSeries( - .init(query: .init( - key: filter.key, - name: filter.name - )) - ) + public func getAmiiboSeries( + by filter: AmiiboSeriesFilter + ) async throws(AmiiboServiceError) -> [AmiiboSeries] { + let response: Operations.getAmiiboSeries.Output + + do { + response = try await client.getAmiiboSeries( + .init(query: .init( + key: filter.key, + name: filter.name + )) + ) + } catch { + throw AmiiboServiceError.unknown + } switch response { case let .ok(ok): @@ -108,13 +122,21 @@ extension AmiiboLiveClient: APIClient { } } - public func getAmiiboTypes(by filter: AmiiboTypeFilter) async throws -> [AmiiboType] { - let response = try await client.getAmiiboTypes( - .init(query: .init( - key: filter.key, - name: filter.name - )) - ) + public func getAmiiboTypes( + by filter: AmiiboTypeFilter + ) async throws(AmiiboServiceError) -> [AmiiboType] { + let response: Operations.getAmiiboTypes.Output + + do { + response = try await client.getAmiiboTypes( + .init(query: .init( + key: filter.key, + name: filter.name + )) + ) + } catch { + throw AmiiboServiceError.unknown + } switch response { case let .ok(ok): @@ -137,13 +159,21 @@ extension AmiiboLiveClient: APIClient { } } - public func getGameCharacters(by filter: GameCharacterFilter) async throws -> [GameCharacter] { - let response = try await client.getGameCharacters( - .init(query: .init( - key: filter.key, - name: filter.name - )) - ) + public func getGameCharacters( + by filter: GameCharacterFilter + ) async throws(AmiiboServiceError) -> [GameCharacter] { + let response: Operations.getGameCharacters.Output + + do { + response = try await client.getGameCharacters( + .init(query: .init( + key: filter.key, + name: filter.name + )) + ) + } catch { + throw AmiiboServiceError.unknown + } switch response { case let .ok(ok): @@ -166,13 +196,21 @@ extension AmiiboLiveClient: APIClient { } } - public func getGameSeries(by filter: GameSeriesFilter) async throws -> [GameSeries] { - let response = try await client.getGameSeries( - .init(query: .init( - key: filter.key, - name: filter.name - )) - ) + public func getGameSeries( + by filter: GameSeriesFilter + ) async throws(AmiiboServiceError) -> [GameSeries] { + let response: Operations.getGameSeries.Output + + do { + response = try await client.getGameSeries( + .init(query: .init( + key: filter.key, + name: filter.name + )) + ) + } catch { + throw AmiiboServiceError.unknown + } switch response { case let .ok(ok): @@ -195,8 +233,14 @@ extension AmiiboLiveClient: APIClient { } } - public func getLastUpdated() async throws -> Date { - let response = try await client.getLastUpdated() + public func getLastUpdated() async throws(AmiiboServiceError) -> Date { + let response: Operations.getLastUpdated.Output + + do { + response = try await client.getLastUpdated() + } catch { + throw AmiiboServiceError.unknown + } switch response { case let .ok(ok): @@ -218,7 +262,12 @@ private extension AmiiboLiveClient { // 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 { case let .Amiibo(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( _ wrapper: Components.Schemas.TupleWrapper, as: Model.Type diff --git a/Sources/Public/Clients/AmiiboMockClient.swift b/Sources/Public/Clients/AmiiboMockClient.swift index e3bb572..86394af 100644 --- a/Sources/Public/Clients/AmiiboMockClient.swift +++ b/Sources/Public/Clients/AmiiboMockClient.swift @@ -12,20 +12,43 @@ import Foundation +/// A type that implements a mock client, for testing purposes. public struct AmiiboMockClient { // MARK: Properties + /// A list of amiibo items to return, if any. private let amiibos: [Amiibo]? + + /// A list of amiibo series to return, if any. private let amiiboSeries: [AmiiboSeries]? + + /// A list of amiibo types to return, if any. private let amiiboTypes: [AmiiboType]? + + /// An error to throw, if any. private let error: AmiiboServiceError? + + /// A list of game characters to return, if any. private let gameCharacters: [GameCharacter]? + + /// A list of game series to return, if any. private let gameSeries: [GameSeries]? + + /// A last updated date to return, if any. 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( amiibos: [Amiibo]? = nil, amiiboSeries: [AmiiboSeries]? = nil, @@ -52,7 +75,7 @@ extension AmiiboMockClient: APIClient { // MARK: Functions - public func getAmiibos(by filter: AmiiboFilter) async throws -> [Amiibo] { + public func getAmiibos(by filter: AmiiboFilter) async throws(AmiiboServiceError) -> [Amiibo] { try throwErrorIfExists() guard let amiibos else { @@ -62,7 +85,7 @@ extension AmiiboMockClient: APIClient { return amiibos } - public func getAmiiboSeries(by filter: AmiiboSeriesFilter) async throws -> [AmiiboSeries] { + public func getAmiiboSeries(by filter: AmiiboSeriesFilter) async throws(AmiiboServiceError) -> [AmiiboSeries] { try throwErrorIfExists() guard let amiiboSeries else { @@ -72,7 +95,7 @@ extension AmiiboMockClient: APIClient { return amiiboSeries } - public func getAmiiboTypes(by filter: AmiiboTypeFilter) async throws -> [AmiiboType] { + public func getAmiiboTypes(by filter: AmiiboTypeFilter) async throws(AmiiboServiceError) -> [AmiiboType] { try throwErrorIfExists() guard let amiiboTypes else { @@ -82,7 +105,7 @@ extension AmiiboMockClient: APIClient { return amiiboTypes } - public func getGameCharacters(by filter: GameCharacterFilter) async throws -> [GameCharacter] { + public func getGameCharacters(by filter: GameCharacterFilter) async throws(AmiiboServiceError) -> [GameCharacter] { try throwErrorIfExists() guard let gameCharacters else { @@ -92,7 +115,7 @@ extension AmiiboMockClient: APIClient { return gameCharacters } - public func getGameSeries(by filter: GameSeriesFilter) async throws -> [GameSeries] { + public func getGameSeries(by filter: GameSeriesFilter) async throws(AmiiboServiceError) -> [GameSeries] { try throwErrorIfExists() guard let gameSeries else { @@ -102,7 +125,7 @@ extension AmiiboMockClient: APIClient { return gameSeries } - public func getLastUpdated() async throws -> Date { + public func getLastUpdated() async throws(AmiiboServiceError) -> Date { try throwErrorIfExists() guard let lastUpdated else { @@ -121,7 +144,9 @@ private extension AmiiboMockClient { // 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 { throw error } diff --git a/Sources/Public/Enumerations/AmiiboClient.swift b/Sources/Public/Enumerations/AmiiboClient.swift new file mode 100644 index 0000000..79efe29 --- /dev/null +++ b/Sources/Public/Enumerations/AmiiboClient.swift @@ -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) +} diff --git a/Sources/Public/Errors/AmiiboServiceError.swift b/Sources/Public/Errors/AmiiboServiceError.swift index 57d32f4..5e26afc 100644 --- a/Sources/Public/Errors/AmiiboServiceError.swift +++ b/Sources/Public/Errors/AmiiboServiceError.swift @@ -10,12 +10,19 @@ // //===----------------------------------------------------------------------=== +/// A representation of all the possible errors that the ``AmiiboService`` service could throw. public enum AmiiboServiceError: Error { + /// A bad request has been given to the client. case badRequest + /// A response cannot be decoded. case decoding + /// An online service is not currently available. case notAvailable + /// A response cannot be found. case notFound + /// An undocumented/unsupported status code error. case undocumented(_ statusCode: Int) + /// An unknown error. case unknown } diff --git a/Sources/Public/Filters/AmiiboFilter.swift b/Sources/Public/Filters/AmiiboFilter.swift index 45b9186..6712ae7 100644 --- a/Sources/Public/Filters/AmiiboFilter.swift +++ b/Sources/Public/Filters/AmiiboFilter.swift @@ -10,21 +10,47 @@ // //===----------------------------------------------------------------------=== +/// A type that contains values to fine-tune a response when requesting amiibo items. public struct AmiiboFilter { // MARK: Properties + /// A game character to return, if any. public let gameCharacter: String? + + /// A game series to return, if any. public let gameSeries: String? + + /// An amiibo identifier to return, if any. public let identifier: String? + + /// An amiibo name to return, if any. public let name: String? + + /// An amiibo series to return, if any. public let series: String? + + /// A flag indicating whether to include games in the response, if any. public let showGames: Bool? + + /// A flag indicating whether to include amiibo usages in games in the response, if any. public let showUsage: Bool? + + /// An amiibo type to return, if any. 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( identifier: String? = nil, name: String? = nil, diff --git a/Sources/Public/Filters/AmiiboSeriesFilter.swift b/Sources/Public/Filters/AmiiboSeriesFilter.swift index c10f4dc..3df3abe 100644 --- a/Sources/Public/Filters/AmiiboSeriesFilter.swift +++ b/Sources/Public/Filters/AmiiboSeriesFilter.swift @@ -10,6 +10,7 @@ // //===----------------------------------------------------------------------=== +/// A type that contains values to fine-tune a response when requesting amiibo series. public struct AmiiboSeriesFilter: KeyNameFilter { // MARK: Properties @@ -17,7 +18,7 @@ public struct AmiiboSeriesFilter: KeyNameFilter { public let key: String? public let name: String? - // MARK: Initialisers + // MARK: Initializers public init() { self.key = nil diff --git a/Sources/Public/Filters/AmiiboTypeFilter.swift b/Sources/Public/Filters/AmiiboTypeFilter.swift index 04d709a..11a6ae3 100644 --- a/Sources/Public/Filters/AmiiboTypeFilter.swift +++ b/Sources/Public/Filters/AmiiboTypeFilter.swift @@ -10,6 +10,7 @@ // //===----------------------------------------------------------------------=== +/// A type that contains values to fine-tune a response when requesting amiibo types. public struct AmiiboTypeFilter: KeyNameFilter { // MARK: Properties @@ -17,7 +18,7 @@ public struct AmiiboTypeFilter: KeyNameFilter { public let key: String? public let name: String? - // MARK: Initialisers + // MARK: Initializers public init() { self.key = nil diff --git a/Sources/Public/Filters/GameCharacterFilter.swift b/Sources/Public/Filters/GameCharacterFilter.swift index 27a2ddf..3298feb 100644 --- a/Sources/Public/Filters/GameCharacterFilter.swift +++ b/Sources/Public/Filters/GameCharacterFilter.swift @@ -10,6 +10,7 @@ // //===----------------------------------------------------------------------=== +/// A type that contains values to fine-tune a response when requesting game characters. public struct GameCharacterFilter: KeyNameFilter { // MARK: Properties @@ -17,7 +18,7 @@ public struct GameCharacterFilter: KeyNameFilter { public let key: String? public let name: String? - // MARK: Initialisers + // MARK: Initializers public init() { self.key = nil diff --git a/Sources/Public/Filters/GameSeriesFilter.swift b/Sources/Public/Filters/GameSeriesFilter.swift index 837a5bb..d45a73f 100644 --- a/Sources/Public/Filters/GameSeriesFilter.swift +++ b/Sources/Public/Filters/GameSeriesFilter.swift @@ -10,6 +10,7 @@ // //===----------------------------------------------------------------------=== +/// A type that contains values to fine-tune a response when requesting game series. public struct GameSeriesFilter: KeyNameFilter { // MARK: Properties @@ -17,7 +18,7 @@ public struct GameSeriesFilter: KeyNameFilter { public let key: String? public let name: String? - // MARK: Initialisers + // MARK: Initializers public init() { self.key = nil diff --git a/Sources/Public/Models/Amiibo.swift b/Sources/Public/Models/Amiibo.swift index a91d5af..eef921a 100644 --- a/Sources/Public/Models/Amiibo.swift +++ b/Sources/Public/Models/Amiibo.swift @@ -12,23 +12,45 @@ import Foundation +/// A model that represents an amiibo item. public struct Amiibo: Sendable { // MARK: Properties + /// A game character. public let gameCharacter: String + + /// A game series. public let gameSeries: String + + /// The first 8 hexadecimal characters of an identifier. public let head: String + + /// An image link. public let image: String + + /// An amiibo name. public let name: String + + /// A game platform type, if any. public let platform: Platform? + + /// A release date. public let release: Release + + /// An amiibo series. public let series: String + + /// The last 8 hexadecimal characters of an identifier. public let tail: String + + /// An amiibo type. 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) { self.gameCharacter = payload.character self.gameSeries = payload.gameSeries @@ -48,10 +70,12 @@ public struct Amiibo: Sendable { // MARK: Computed + /// An identifier. public var identifier: String { head + tail } + /// A URL related to an image link, if any. 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 index fe95824..f4e3be9 100644 --- a/Sources/Public/Models/Amiibo/Amiibo+Game.swift +++ b/Sources/Public/Models/Amiibo/Amiibo+Game.swift @@ -11,16 +11,24 @@ //===----------------------------------------------------------------------=== extension Amiibo { + /// A model that represents a game related to an amiibo item. public struct Game: Sendable { // MARK: Properties + /// A list of identifiers. public let identifiers: [String] + + /// A name. public let name: String + + /// A list of amiibo usages, if any. 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) { self.identifiers = payload.gameID self.name = payload.gameName diff --git a/Sources/Public/Models/Amiibo/Amiibo+Platform.swift b/Sources/Public/Models/Amiibo/Amiibo+Platform.swift index 283cf06..116798f 100644 --- a/Sources/Public/Models/Amiibo/Amiibo+Platform.swift +++ b/Sources/Public/Models/Amiibo/Amiibo+Platform.swift @@ -11,16 +11,30 @@ //===----------------------------------------------------------------------=== extension Amiibo { + /// A model that represents a collection of `WiiU`, `3DS`, and `Switch` games related to an amiibo item. public struct Platform: Sendable { // MARK: Properties + /// A list of `Switch` games related to an amiibo item. public let `switch`: [Game] + + /// A list of `3DS` games related to an amiibo item. public let threeDS: [Game] + + /// A list of `WiiU` games related to an amiibo item. public let wiiU: [Game] // 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?( _ `switch`: [Components.Schemas.AmiiboGame]?, _ threeDS: [Components.Schemas.AmiiboGame]?, diff --git a/Sources/Public/Models/Amiibo/Amiibo+Release.swift b/Sources/Public/Models/Amiibo/Amiibo+Release.swift index 6357fe0..6c2c302 100644 --- a/Sources/Public/Models/Amiibo/Amiibo+Release.swift +++ b/Sources/Public/Models/Amiibo/Amiibo+Release.swift @@ -13,17 +13,27 @@ import Foundation extension Amiibo { + /// A model that represents a collection of release dates related to an amiibo item. public struct Release: Sendable { // MARK: Properties + /// A release date for North America, if any. public let america: Date? + + /// A release date for Australia, if any. public let australia: Date? + + /// A release date for Europe, if any. public let europe: Date? + + /// A release date for Japan, if any. 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) { self.america = payload.na self.australia = payload.au diff --git a/Sources/Public/Models/Amiibo/Amiibo+Usage.swift b/Sources/Public/Models/Amiibo/Amiibo+Usage.swift index abfd898..68bda05 100644 --- a/Sources/Public/Models/Amiibo/Amiibo+Usage.swift +++ b/Sources/Public/Models/Amiibo/Amiibo+Usage.swift @@ -11,15 +11,21 @@ //===----------------------------------------------------------------------=== extension Amiibo { + /// A model that represents the usage of an amiibo item within a certain game. public struct Usage: Sendable { // MARK: Properties + /// An explanation of how to use an amiibo item. public let explanation: String + + /// A flag that indicates whether an amiibo item can save game data in it. 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) { self.explanation = payload.Usage self.isWriteable = payload.write diff --git a/Sources/Public/Models/AmiiboSeries.swift b/Sources/Public/Models/AmiiboSeries.swift index ac734be..92eefff 100644 --- a/Sources/Public/Models/AmiiboSeries.swift +++ b/Sources/Public/Models/AmiiboSeries.swift @@ -10,6 +10,7 @@ // //===----------------------------------------------------------------------=== +/// A model that represents an amiibo series. public struct AmiiboSeries: KeyNameModel { // MARK: Properties @@ -17,7 +18,7 @@ public struct AmiiboSeries: KeyNameModel { public let key: String public let name: String - // MARK: Initialisers + // MARK: Initializers init(_ payload: Components.Schemas.Tuple) { self.key = payload.key diff --git a/Sources/Public/Models/AmiiboType.swift b/Sources/Public/Models/AmiiboType.swift index a9f0043..11f5235 100644 --- a/Sources/Public/Models/AmiiboType.swift +++ b/Sources/Public/Models/AmiiboType.swift @@ -10,6 +10,7 @@ // //===----------------------------------------------------------------------=== +/// A model that represents an amiibo type. public struct AmiiboType: KeyNameModel { // MARK: Properties @@ -17,7 +18,7 @@ public struct AmiiboType: KeyNameModel { public let key: String public let name: String - // MARK: Initialisers + // MARK: Initializers init(_ payload: Components.Schemas.Tuple) { self.key = payload.key diff --git a/Sources/Public/Models/GameCharacter.swift b/Sources/Public/Models/GameCharacter.swift index 62df719..1189b4a 100644 --- a/Sources/Public/Models/GameCharacter.swift +++ b/Sources/Public/Models/GameCharacter.swift @@ -10,6 +10,7 @@ // //===----------------------------------------------------------------------=== +/// A model that represents a game character. public struct GameCharacter: KeyNameModel { // MARK: Properties @@ -17,7 +18,7 @@ public struct GameCharacter: KeyNameModel { public let key: String public let name: String - // MARK: Initialisers + // MARK: Initializers init(_ payload: Components.Schemas.Tuple) { self.key = payload.key diff --git a/Sources/Public/Models/GameSeries.swift b/Sources/Public/Models/GameSeries.swift index 006e3df..40b03aa 100644 --- a/Sources/Public/Models/GameSeries.swift +++ b/Sources/Public/Models/GameSeries.swift @@ -10,6 +10,7 @@ // //===----------------------------------------------------------------------=== +/// A model that represents a game series. public struct GameSeries: KeyNameModel { // MARK: Properties @@ -17,7 +18,7 @@ public struct GameSeries: KeyNameModel { public let key: String public let name: String - // MARK: Initialisers + // MARK: Initializers init(_ payload: Components.Schemas.Tuple) { self.key = payload.key diff --git a/Sources/Public/Services/AmiiboService.swift b/Sources/Public/Services/AmiiboService.swift index 2b9c508..8fea32e 100644 --- a/Sources/Public/Services/AmiiboService.swift +++ b/Sources/Public/Services/AmiiboService.swift @@ -12,51 +12,81 @@ import Foundation +/// A type that implements the service that uses a client to make calls. public struct AmiiboService { // MARK: Properties + /// A client to interact with the endpoints. private let client: any APIClient - // MARK: Initialisers - - public init(_ client: any APIClient) { - self.client = client + // MARK: Initializers + + /// Initializes this service with a specific client type. + /// - 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 - + + /// 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( _ filter: AmiiboFilter = .init() - ) async throws -> [Amiibo] { + ) async throws(AmiiboServiceError) -> [Amiibo] { 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( _ filter: AmiiboSeriesFilter = .init() - ) async throws -> [AmiiboSeries] { + ) async throws(AmiiboServiceError) -> [AmiiboSeries] { 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( _ filter: AmiiboTypeFilter = .init() - ) async throws -> [AmiiboType] { + ) async throws(AmiiboServiceError) -> [AmiiboType] { 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( _ filter: GameCharacterFilter = .init() - ) async throws -> [GameCharacter] { + ) async throws(AmiiboServiceError) -> [GameCharacter] { 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( _ filter: GameSeriesFilter = .init() - ) async throws -> [GameSeries] { + ) async throws(AmiiboServiceError) -> [GameSeries] { 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() } diff --git a/Tests/AmiiboServiceLiveTests.swift b/Tests/Public/Services/AmiiboServiceLiveTests.swift similarity index 99% rename from Tests/AmiiboServiceLiveTests.swift rename to Tests/Public/Services/AmiiboServiceLiveTests.swift index 7c581ee..25d4774 100644 --- a/Tests/AmiiboServiceLiveTests.swift +++ b/Tests/Public/Services/AmiiboServiceLiveTests.swift @@ -23,10 +23,8 @@ struct AmiiboServiceLiveTests { // MARK: Initialisers - init() throws { - let client = try AmiiboLiveClient() - - self.service = .init(client) + init() { + self.service = .init(.live()) } // MARK: Functions tests