diff --git a/.env b/.env index f7650bb26c..163f9185d1 100644 --- a/.env +++ b/.env @@ -15,6 +15,7 @@ # --- DOCUMENTATION --- DOCC_ARCHIVE_OUTPUT=./${SPM_LIBRARY_TARGET}.doccarchive +DOCC_CATALOG=Sources/$(SPM_LIBRARY_TARGET)/Catalogs/$(SPM_LIBRARY_TARGET).docc DOCC_GITHUB_OUTPUT=./docs DOCC_GITHUB_BASE_PATH=asconnect-service DOCC_PREVIEW_URL=http://localhost:8080/documentation/asconnectservice diff --git a/Makefile b/Makefile index 9b39355a2a..5afa3729a7 100644 --- a/Makefile +++ b/Makefile @@ -82,6 +82,9 @@ doc-generate-github: ## Generates the library documentation for Github --hosting-base-path $(DOCC_GITHUB_BASE_PATH) \ --output-path $(DOCC_GITHUB_OUTPUT) +doc-check: ## Checks the DocC catalog covers every generated operation and schema + @python3 -I Scripts/check-docc.py "$(OAS_GENERATE_OUTPUT)" "$(DOCC_CATALOG)" + doc-preview: ## Previews the library documentation in Safari @open -a safari $(DOCC_PREVIEW_URL) @ENABLE_DOCC_PLUGIN=1 swift package \ @@ -101,13 +104,16 @@ oas-download: ## Downloads the latest App Store Connect OpenAPI specification do @unzip -q "$(OAS_DOWNLOAD_INPUT)" -d "$(OAS_DOWNLOAD_FOLDER)" @mv "$(OAS_DOWNLOAD_FILE)" "$(OAS_DOWNLOAD_OUTPUT)" @rm -rf "$(OAS_DOWNLOAD_FOLDER)" - @$(MAKE) --no-print-directory oas-patch + @# Generating the code also validates the document. + @$(MAKE) --no-print-directory oas-patch-document @$(MAKE) --no-print-directory oas-generate + @$(MAKE) --no-print-directory doc-check -oas-patch: # Patches the OpenAPI specification document with the patches in Scripts/patch-oas.jq +oas-patch: oas-patch-document oas-validate # Patches and validates the OpenAPI specification document + +oas-patch-document: # Patches the OpenAPI specification document with Scripts/patch-oas.jq @jq --from-file Scripts/patch-oas.jq "$(OAS_DOWNLOAD_OUTPUT)" > "$(OAS_DOWNLOAD_OUTPUT).tmp" @mv "$(OAS_DOWNLOAD_OUTPUT).tmp" "$(OAS_DOWNLOAD_OUTPUT)" - @$(MAKE) --no-print-directory oas-validate oas-validate: ## Validates the OpenAPI specification document @swift run \ diff --git a/README.md b/README.md index 163903f59e..c8311d0aaa 100644 --- a/README.md +++ b/README.md @@ -31,33 +31,31 @@ Then add it to your target dependencies: ### Creating a Client -Create a `Client` with a server URL, a transport, and a `BearerAuthMiddleware` that authenticates each request with a JSON Web Token (JWT). To get a token, create an API key in App Store Connect and sign a JWT with its key ID, issuer ID, and private key: +Create a `Client` with a server URL, a transport, and a `BearerAuthMiddleware`. With an API key from App Store Connect, `APIKeyTokenProvider` signs the JSON Web Tokens (JWT) the API requires, and renews them before they expire: ```swift import ASConnectService import OpenAPIURLSession -let client = Client( - serverURL: try Servers.Server1.url(), - transport: URLSessionTransport(), - middlewares: [ - BearerAuthMiddleware(token: yourJWTToken) - ] -) -``` - -Tokens expire after 20 minutes at most. For longer-lived clients, use `APIKeyTokenProvider`, which signs new ones with your API key as needed: - -```swift let tokenProvider = try APIKeyTokenProvider( issuerID: yourIssuerID, keyID: yourKeyID, privateKey: yourP8FileContents ) -BearerAuthMiddleware { - try await tokenProvider.token() -} +let client = Client( + serverURL: try Servers.Server1.url(), + transport: URLSessionTransport(), + middlewares: [ + BearerAuthMiddleware { try await tokenProvider.token() } + ] +) +``` + +To use a token you signed yourself, pass it directly. It expires after 20 minutes at most: + +```swift +BearerAuthMiddleware(token: yourJWTToken) ``` ### Making API Calls @@ -97,8 +95,9 @@ middlewares: [ ] ``` -## Supported Platforms +## Requirements +- Swift 6.0+ (Xcode 16+) - iOS 14.0+ - macOS 11.0+ - tvOS 14.0+ @@ -130,6 +129,9 @@ make doc-generate-github # Preview documentation locally in Safari make doc-preview + +# Check the catalog covers the generated code +make doc-check ``` ### Updating the API diff --git a/Scripts/check-docc.py b/Scripts/check-docc.py new file mode 100644 index 0000000000..9b178b4ae5 --- /dev/null +++ b/Scripts/check-docc.py @@ -0,0 +1,83 @@ +# ===------------------------------------------------------------------------------------=== +# +# This source file is part of the App Store Connect Service open source project +# +# Copyright (c) 2026 Röck+Cöde VoF. and the App Store Connect Service project authors +# Licensed under Apache license v2.0 +# +# See LICENSE for license information +# See CONTRIBUTORS for the list of App Store Connect Service project authors +# +# SPDX-License-Identifier: Apache-2.0 +# +# ===------------------------------------------------------------------------------------=== + +"""Checks that the DocC catalog lists every generated operation and schema, and nothing else. + +Usage: python3 check-docc.py +""" + +import re +import sys + + +def generated_symbols(types_path): + """Returns the methods, operations, and schemas in the generated `Types.swift` file.""" + lines = open(types_path).read().split('\n') + + def start(prefix): + return next(index for index, line in enumerate(lines) if line.startswith(prefix)) + + protocol = lines[start('public protocol APIProtocol'):start('extension APIProtocol {')] + components = lines[start('public enum Components {'):start('public enum Operations {')] + operations = lines[start('public enum Operations {'):] + + schema = re.compile(r' (?:@\w+ )*public (?:indirect )?(?:struct|enum|typealias) (\w+)') + + return { + 'APIProtocol.md': {m.group(1) for line in protocol if (m := re.match(r' func (\w+)\(', line))}, + 'Operations.md': {m.group(1) for line in operations if (m := re.match(r' public enum (\w+)', line))}, + 'Components.md': {m.group(1) for line in components if (m := schema.match(line))}, + } + + +def documented_symbols(catalog_path): + """Returns the symbols listed in the DocC catalog topics.""" + patterns = { + 'APIProtocol.md': r'``(\w+)\(_:\)``', + 'Operations.md': r'``Operations/(\w+)``', + 'Components.md': r'``Components/Schemas/(\w+)``', + } + + return { + name: set(re.findall(pattern, open(f'{catalog_path}/Extensions/{name}').read())) + for name, pattern in patterns.items() + } + + +def main(generated_path, catalog_path): + generated = generated_symbols(f'{generated_path}/Types.swift') + documented = documented_symbols(catalog_path) + is_up_to_date = True + + for name in generated: + missing = sorted(generated[name] - documented[name]) + stale = sorted(documented[name] - generated[name]) + + for title, symbols in [('Missing from', missing), ('Stale in', stale)]: + if symbols: + is_up_to_date = False + print(f'{title} {name} ({len(symbols)}):') + print('\n'.join(f' - {symbol}' for symbol in symbols)) + + if is_up_to_date: + print('The DocC catalog lists every generated operation and schema.') + + return 0 if is_up_to_date else 1 + + +if __name__ == '__main__': + if len(sys.argv) != 3: + sys.exit(__doc__) + + sys.exit(main(sys.argv[1], sys.argv[2])) diff --git a/Sources/ASConnectService/Catalogs/ASConnectService.docc/Library.md b/Sources/ASConnectService/Catalogs/ASConnectService.docc/Library.md index fd48910970..2ba24103b1 100644 --- a/Sources/ASConnectService/Catalogs/ASConnectService.docc/Library.md +++ b/Sources/ASConnectService/Catalogs/ASConnectService.docc/Library.md @@ -8,33 +8,31 @@ ASConnectService exposes every endpoint of Apple's App Store Connect API as a Sw ### Creating a Client -Create a ``Client`` with a server URL, a transport, and a ``BearerAuthMiddleware`` that authenticates each request with a JSON Web Token (JWT): +Create a ``Client`` with a server URL, a transport, and a ``BearerAuthMiddleware``. With an API key from App Store Connect, ``APIKeyTokenProvider`` signs the JSON Web Tokens (JWT) the API requires, and renews them before they expire: ```swift import ASConnectService import OpenAPIURLSession -let client = Client( - serverURL: try Servers.Server1.url(), - transport: URLSessionTransport(), - middlewares: [ - BearerAuthMiddleware(token: yourJWTToken) - ] -) -``` - -Tokens expire after 20 minutes at most. For longer-lived clients, use ``APIKeyTokenProvider``, which signs new ones with your API key as needed: - -```swift let tokenProvider = try APIKeyTokenProvider( issuerID: yourIssuerID, keyID: yourKeyID, privateKey: yourP8FileContents ) -BearerAuthMiddleware { - try await tokenProvider.token() -} +let client = Client( + serverURL: try Servers.Server1.url(), + transport: URLSessionTransport(), + middlewares: [ + BearerAuthMiddleware { try await tokenProvider.token() } + ] +) +``` + +To use a token you signed yourself, pass it directly. It expires after 20 minutes at most: + +```swift +BearerAuthMiddleware(token: yourJWTToken) ``` ### Making API Calls diff --git a/Sources/ASConnectService/Sources/Public/Generated/Client.swift b/Sources/ASConnectService/Sources/Public/Generated/Client.swift index bc12832b15..b16f934fdd 100644 --- a/Sources/ASConnectService/Sources/Public/Generated/Client.swift +++ b/Sources/ASConnectService/Sources/Public/Generated/Client.swift @@ -1,4 +1,5 @@ // Generated by swift-openapi-generator, do not modify. +// swift-format-ignore-file @_spi(Generated) import OpenAPIRuntime #if os(Linux) @preconcurrency import struct Foundation.URL diff --git a/Sources/ASConnectService/Sources/Public/Generated/Types.swift b/Sources/ASConnectService/Sources/Public/Generated/Types.swift index 0c0ffb9af5..f07d7c25c5 100644 --- a/Sources/ASConnectService/Sources/Public/Generated/Types.swift +++ b/Sources/ASConnectService/Sources/Public/Generated/Types.swift @@ -1,4 +1,5 @@ // Generated by swift-openapi-generator, do not modify. +// swift-format-ignore-file @_spi(Generated) import OpenAPIRuntime #if os(Linux) @preconcurrency import struct Foundation.URL diff --git a/Sources/ASConnectService/openapi-generator-config.yaml b/Sources/ASConnectService/openapi-generator-config.yaml index 03169543dc..d6393f3ec6 100644 --- a/Sources/ASConnectService/openapi-generator-config.yaml +++ b/Sources/ASConnectService/openapi-generator-config.yaml @@ -17,3 +17,6 @@ generate: - client namingStrategy: idiomatic accessModifier: public +# Stops swift-format from reformatting the generated code. +additionalFileComments: + - swift-format-ignore-file diff --git a/Tests/ASConnectService/Cases/Public/Middlewares/BearerAuthMiddlewareTests.swift b/Tests/ASConnectService/Cases/Public/Middlewares/BearerAuthMiddlewareTests.swift index 64bf01cb9e..48586c8f41 100644 --- a/Tests/ASConnectService/Cases/Public/Middlewares/BearerAuthMiddlewareTests.swift +++ b/Tests/ASConnectService/Cases/Public/Middlewares/BearerAuthMiddlewareTests.swift @@ -47,17 +47,14 @@ struct BearerAuthMiddlewareTests { let middleware = BearerAuthMiddleware(token: .Token.jwt) let requestBody: HTTPBody = HTTPBody("request-body") - var receivedBody: HTTPBody? - - _ = try await middleware.intercept( + // The next handler echoes the body back, so the test can read it. + let (_, receivedBody) = try await middleware.intercept( .get, body: requestBody, baseURL: .base, operationID: "createApp", next: { _, body, _ in - receivedBody = body - - return (HTTPResponse(status: .created), nil) + (HTTPResponse(status: .created), body) } )