Skip to content

Commit b5d49a6

Browse files
authored
Improve OpenAPI error handling (#78)
- Make some enums non-exhaustive (closes #77) - Also a tentative fix for #62 and #64 - Improve logging on deserialization failure We now use raw JS to patch the OpenAPI spec instead of openapi-overlay. It's a lot more flexible this way.
1 parent cd3d72c commit b5d49a6

12 files changed

Lines changed: 352 additions & 117 deletions

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,8 @@ api: openapi/openapi.json
144144
update-api:
145145
@+$(MAKE) -B api
146146

147-
openapi/openapi.json: openapi/base.json Sources/DeveloperAPI/openapi-overlay.yaml
148-
npx --yes bump-cli overlay openapi/base.json Sources/DeveloperAPI/openapi-overlay.yaml > openapi/openapi.json
147+
openapi/openapi.json: openapi/base.json Sources/DeveloperAPI/patch.js
148+
node Sources/DeveloperAPI/patch.js < openapi/base.json > openapi/openapi.json
149149

150150
openapi/base.json:
151151
@mkdir -p openapi

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ let package = Package(
8888
dependencies: [
8989
.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
9090
],
91-
exclude: ["openapi-generator-config.yaml", "openapi-overlay.yaml"]
91+
exclude: ["openapi-generator-config.yaml", "patch.js"]
9292
),
9393
.target(
9494
name: "XKit",

Sources/DeveloperAPI/Generated/Types.swift

Lines changed: 247 additions & 62 deletions
Large diffs are not rendered by default.

Sources/DeveloperAPI/openapi-overlay.yaml

Lines changed: 0 additions & 42 deletions
This file was deleted.

Sources/DeveloperAPI/patch.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/usr/bin/env node
2+
3+
import fs from 'fs';
4+
5+
function makeOpen(enumSchema) {
6+
const copy = structuredClone(enumSchema);
7+
Object.keys(enumSchema).forEach(k => delete enumSchema[k]);
8+
Object.assign(enumSchema, {
9+
anyOf: [
10+
copy,
11+
{ type: 'string' },
12+
]
13+
})
14+
}
15+
16+
function format(schema) {
17+
const schemas = schema.components.schemas;
18+
19+
// this field is required when using the private Xcode API
20+
const capabilityCreateRelationships = schemas.BundleIdCapabilityCreateRequest.properties.data.properties.relationships;
21+
capabilityCreateRelationships.properties.capability = {
22+
type: 'object',
23+
properties: {
24+
data: {
25+
type: 'object',
26+
properties: {
27+
type: {
28+
type: 'string',
29+
enum: ['capabilities'],
30+
},
31+
id: { $ref: '#/components/schemas/CapabilityType' },
32+
},
33+
required: ['id', 'type'],
34+
},
35+
},
36+
required: ['data'],
37+
}
38+
capabilityCreateRelationships.required.push('capability');
39+
40+
// we don't use this but it triggers a deprecation warning. see:
41+
// https://github.com/apple/swift-openapi-generator/issues/715
42+
schemas.App.properties.relationships.properties.inAppPurchases.deprecated = false;
43+
44+
// openapi-generator expects response enums to be exhaustive. Apple's ASC OpenAPI spec
45+
// misses some cases that they do, actually, return.
46+
// https://swiftpackageindex.com/apple/swift-openapi-generator/1.7.2/documentation/swift-openapi-generator/useful-openapi-patterns#Open-enums-and-oneOfs
47+
makeOpen(schemas.BundleIdPlatform);
48+
makeOpen(schemas.CapabilityType);
49+
makeOpen(schemas.CertificateType);
50+
makeOpen(schemas.Device.properties.attributes.properties.deviceClass);
51+
52+
return schema;
53+
}
54+
55+
const text = fs.readFileSync(process.stdin.fd, 'utf8');
56+
const json = JSON.parse(text);
57+
const formatted = format(json);
58+
const formattedText = JSON.stringify(formatted);
59+
console.log(formattedText);

Sources/XKit/DeveloperServices/App IDs/DeveloperServicesAddAppOperation.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public struct DeveloperServicesAddAppOperation: DeveloperServicesOperation {
5757
_type: .bundleIds,
5858
attributes: .init(
5959
name: name,
60-
platform: .ios,
60+
platform: .init(.ios),
6161
identifier: newBundleID
6262
)
6363
)
@@ -105,8 +105,8 @@ public struct DeveloperServicesAddAppOperation: DeveloperServicesOperation {
105105
}
106106
} else {
107107
// DeveloperServices doesn't allow deleting these capabilities
108-
let requiredCapabilities: Set<Components.Schemas.CapabilityType> = [.inAppPurchase]
109-
if let capType = cap.attributes?.capabilityType, !requiredCapabilities.contains(capType) {
108+
let requiredCapabilities: Set<Components.Schemas.CapabilityType.Value1Payload> = [.inAppPurchase]
109+
if let capType = cap.attributes?.capabilityType?.value1, !requiredCapabilities.contains(capType) {
110110
_ = try await context.developerAPIClient
111111
.bundleIdCapabilitiesDeleteInstance(path: .init(id: cap.id))
112112
.noContent

Sources/XKit/DeveloperServices/App IDs/Entitlements/DeveloperServicesCapability.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ public struct DeveloperServicesCapability: Sendable, Hashable {
1515
public var settings: [Components.Schemas.CapabilitySetting]?
1616

1717
public init(
18-
_ capabilityType: Components.Schemas.CapabilityType,
18+
_ capabilityType: Components.Schemas.CapabilityType.Value1Payload,
1919
isFree: Bool,
2020
settings: [Components.Schemas.CapabilitySetting]? = nil
2121
) {
22-
self.capabilityType = capabilityType
22+
self.capabilityType = .init(capabilityType)
2323
self.isFree = isFree
2424
self.settings = settings
2525
}

Sources/XKit/DeveloperServices/Certificates/DeveloperServicesFetchCertificateOperation.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public struct DeveloperServicesFetchCertificateOperation: DeveloperServicesOpera
5454
_type: .certificates,
5555
attributes: .init(
5656
csrContent: csr.pemString,
57-
certificateType: .development
57+
certificateType: .init(.development)
5858
)
5959
)))
6060
)

Sources/XKit/DeveloperServices/DeveloperServices+OpenAPI.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,3 +256,27 @@ struct LoggingMiddleware: ClientMiddleware {
256256
return (response, body)
257257
}
258258
}
259+
260+
// syntactic sugar to make it nicer to work with `anyOf:` types
261+
public protocol OpenAPIExtensibleEnum {
262+
associatedtype Value1Payload: RawRepresentable<String>, Codable, Hashable, Sendable, CaseIterable
263+
var value1: Value1Payload? { get set }
264+
var value2: String? { get set }
265+
266+
init(value1: Value1Payload?, value2: String?)
267+
}
268+
269+
extension OpenAPIExtensibleEnum {
270+
public var rawValue: String {
271+
value2!
272+
}
273+
274+
public init(_ value: Value1Payload) {
275+
self.init(value1: value, value2: nil)
276+
}
277+
}
278+
279+
extension Components.Schemas.BundleIdPlatform: OpenAPIExtensibleEnum {}
280+
extension Components.Schemas.CapabilityType: OpenAPIExtensibleEnum {}
281+
extension Components.Schemas.CertificateType: OpenAPIExtensibleEnum {}
282+
extension Components.Schemas.Device.AttributesPayload.DeviceClassPayload: OpenAPIExtensibleEnum {}

Sources/XKit/DeveloperServices/Devices/DeveloperServicesFetchDeviceOperation.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public struct DeveloperServicesFetchDeviceOperation: DeveloperServicesOperation
2727
_type: .devices,
2828
attributes: .init(
2929
name: self.context.deviceName,
30-
platform: .ios,
30+
platform: .init(.ios),
3131
udid: self.context.udid
3232
)
3333
)))

0 commit comments

Comments
 (0)