Overhauled the library with several new features #5
No files matched your search
@@ -15,6 +15,7 @@
|
|||||||
# --- DOCUMENTATION ---
|
# --- DOCUMENTATION ---
|
||||||
|
|
||||||
DOCC_ARCHIVE_OUTPUT=./${SPM_LIBRARY_TARGET}.doccarchive
|
DOCC_ARCHIVE_OUTPUT=./${SPM_LIBRARY_TARGET}.doccarchive
|
||||||
|
DOCC_CATALOG=Sources/$(SPM_LIBRARY_TARGET)/Catalogs/$(SPM_LIBRARY_TARGET).docc
|
||||||
DOCC_GITHUB_OUTPUT=./docs
|
DOCC_GITHUB_OUTPUT=./docs
|
||||||
DOCC_GITHUB_BASE_PATH=asconnect-service
|
DOCC_GITHUB_BASE_PATH=asconnect-service
|
||||||
DOCC_PREVIEW_URL=http://localhost:8080/documentation/asconnectservice
|
DOCC_PREVIEW_URL=http://localhost:8080/documentation/asconnectservice
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ doc-generate-github: ## Generates the library documentation for Github
|
|||||||
--hosting-base-path $(DOCC_GITHUB_BASE_PATH) \
|
--hosting-base-path $(DOCC_GITHUB_BASE_PATH) \
|
||||||
--output-path $(DOCC_GITHUB_OUTPUT)
|
--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
|
doc-preview: ## Previews the library documentation in Safari
|
||||||
@open -a safari $(DOCC_PREVIEW_URL)
|
@open -a safari $(DOCC_PREVIEW_URL)
|
||||||
@ENABLE_DOCC_PLUGIN=1 swift package \
|
@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)"
|
@unzip -q "$(OAS_DOWNLOAD_INPUT)" -d "$(OAS_DOWNLOAD_FOLDER)"
|
||||||
@mv "$(OAS_DOWNLOAD_FILE)" "$(OAS_DOWNLOAD_OUTPUT)"
|
@mv "$(OAS_DOWNLOAD_FILE)" "$(OAS_DOWNLOAD_OUTPUT)"
|
||||||
@rm -rf "$(OAS_DOWNLOAD_FOLDER)"
|
@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 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"
|
@jq --from-file Scripts/patch-oas.jq "$(OAS_DOWNLOAD_OUTPUT)" > "$(OAS_DOWNLOAD_OUTPUT).tmp"
|
||||||
@mv "$(OAS_DOWNLOAD_OUTPUT).tmp" "$(OAS_DOWNLOAD_OUTPUT)"
|
@mv "$(OAS_DOWNLOAD_OUTPUT).tmp" "$(OAS_DOWNLOAD_OUTPUT)"
|
||||||
@$(MAKE) --no-print-directory oas-validate
|
|
||||||
|
|
||||||
oas-validate: ## Validates the OpenAPI specification document
|
oas-validate: ## Validates the OpenAPI specification document
|
||||||
@swift run \
|
@swift run \
|
||||||
|
|||||||
@@ -31,33 +31,31 @@ Then add it to your target dependencies:
|
|||||||
|
|
||||||
### Creating a Client
|
### 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
|
```swift
|
||||||
import ASConnectService
|
import ASConnectService
|
||||||
import OpenAPIURLSession
|
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(
|
let tokenProvider = try APIKeyTokenProvider(
|
||||||
issuerID: yourIssuerID,
|
issuerID: yourIssuerID,
|
||||||
keyID: yourKeyID,
|
keyID: yourKeyID,
|
||||||
privateKey: yourP8FileContents
|
privateKey: yourP8FileContents
|
||||||
)
|
)
|
||||||
|
|
||||||
BearerAuthMiddleware {
|
let client = Client(
|
||||||
try await tokenProvider.token()
|
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
|
### Making API Calls
|
||||||
@@ -97,8 +95,9 @@ middlewares: [
|
|||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Supported Platforms
|
## Requirements
|
||||||
|
|
||||||
|
- Swift 6.0+ (Xcode 16+)
|
||||||
- iOS 14.0+
|
- iOS 14.0+
|
||||||
- macOS 11.0+
|
- macOS 11.0+
|
||||||
- tvOS 14.0+
|
- tvOS 14.0+
|
||||||
@@ -130,6 +129,9 @@ make doc-generate-github
|
|||||||
|
|
||||||
# Preview documentation locally in Safari
|
# Preview documentation locally in Safari
|
||||||
make doc-preview
|
make doc-preview
|
||||||
|
|
||||||
|
# Check the catalog covers the generated code
|
||||||
|
make doc-check
|
||||||
```
|
```
|
||||||
|
|
||||||
### Updating the API
|
### Updating the API
|
||||||
|
|||||||
@@ -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 <generated sources folder> <DocC catalog folder>
|
||||||
|
"""
|
||||||
|
|
||||||
|
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]))
|
||||||
@@ -8,33 +8,31 @@ ASConnectService exposes every endpoint of Apple's App Store Connect API as a Sw
|
|||||||
|
|
||||||
### Creating a Client
|
### 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
|
```swift
|
||||||
import ASConnectService
|
import ASConnectService
|
||||||
import OpenAPIURLSession
|
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(
|
let tokenProvider = try APIKeyTokenProvider(
|
||||||
issuerID: yourIssuerID,
|
issuerID: yourIssuerID,
|
||||||
keyID: yourKeyID,
|
keyID: yourKeyID,
|
||||||
privateKey: yourP8FileContents
|
privateKey: yourP8FileContents
|
||||||
)
|
)
|
||||||
|
|
||||||
BearerAuthMiddleware {
|
let client = Client(
|
||||||
try await tokenProvider.token()
|
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
|
### Making API Calls
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// Generated by swift-openapi-generator, do not modify.
|
// Generated by swift-openapi-generator, do not modify.
|
||||||
|
// swift-format-ignore-file
|
||||||
@_spi(Generated) import OpenAPIRuntime
|
@_spi(Generated) import OpenAPIRuntime
|
||||||
#if os(Linux)
|
#if os(Linux)
|
||||||
@preconcurrency import struct Foundation.URL
|
@preconcurrency import struct Foundation.URL
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// Generated by swift-openapi-generator, do not modify.
|
// Generated by swift-openapi-generator, do not modify.
|
||||||
|
// swift-format-ignore-file
|
||||||
@_spi(Generated) import OpenAPIRuntime
|
@_spi(Generated) import OpenAPIRuntime
|
||||||
#if os(Linux)
|
#if os(Linux)
|
||||||
@preconcurrency import struct Foundation.URL
|
@preconcurrency import struct Foundation.URL
|
||||||
|
|||||||
@@ -17,3 +17,6 @@ generate:
|
|||||||
- client
|
- client
|
||||||
namingStrategy: idiomatic
|
namingStrategy: idiomatic
|
||||||
accessModifier: public
|
accessModifier: public
|
||||||
|
# Stops swift-format from reformatting the generated code.
|
||||||
|
additionalFileComments:
|
||||||
|
- swift-format-ignore-file
|
||||||
@@ -47,17 +47,14 @@ struct BearerAuthMiddlewareTests {
|
|||||||
let middleware = BearerAuthMiddleware(token: .Token.jwt)
|
let middleware = BearerAuthMiddleware(token: .Token.jwt)
|
||||||
let requestBody: HTTPBody = HTTPBody("request-body")
|
let requestBody: HTTPBody = HTTPBody("request-body")
|
||||||
|
|
||||||
var receivedBody: HTTPBody?
|
// The next handler echoes the body back, so the test can read it.
|
||||||
|
let (_, receivedBody) = try await middleware.intercept(
|
||||||
_ = try await middleware.intercept(
|
|
||||||
.get,
|
.get,
|
||||||
body: requestBody,
|
body: requestBody,
|
||||||
baseURL: .base,
|
baseURL: .base,
|
||||||
operationID: "createApp",
|
operationID: "createApp",
|
||||||
next: { _, body, _ in
|
next: { _, body, _ in
|
||||||
receivedBody = body
|
(HTTPResponse(status: .created), body)
|
||||||
|
|
||||||
return (HTTPResponse(status: .created), nil)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user