Skip to content

Commit bff24af

Browse files
committed
Support "webhookPingCreated" type payload
1 parent fb9a1cf commit bff24af

10 files changed

Lines changed: 296 additions & 21 deletions

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import PackageDescription
44

55
let package = Package(
66
name: "AppStoreConnectWebhook",
7+
platforms: [.macOS(.v12)],
78
products: [
89
.library(
910
name: "AppStoreConnectWebhook",

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,33 @@ Type definitions for App Store Connect webhook notification payloads in Swift.
88
[![X (formerly Twitter): @treastrain](https://img.shields.io/twitter/follow/treastrain?label=%40treastrain&style=social)](https://x.com/treastrain)
99
[![Swift Build & Test](https://github.com/treastrain/AppStoreConnectWebhook/actions/workflows/swift.yml/badge.svg)](https://github.com/treastrain/AppStoreConnectWebhook/actions/workflows/swift.yml)
1010

11+
# Usage
12+
```swift
13+
import AppStoreConnectWebhook
14+
import Foundation
15+
16+
let payloadJSON = """
17+
{
18+
"data": {
19+
"type": "webhookPingCreated",
20+
"id": "01234567-abcd-8901-dcba-987654321012",
21+
"version": 1,
22+
"attributes": {
23+
"timestamp": "2025-06-09T10:09:30.123456789Z"
24+
}
25+
}
26+
}
27+
"""
28+
29+
let payload = try AppStoreConnectWebhookPayload(json: payloadJSON)
30+
switch payload.data {
31+
case .webhookPingCreated(let webhookPingCreated):
32+
webhookPingCreated.id // "01234567-abcd-8901-dcba-987654321012"
33+
webhookPingCreated.attributes.timestamp // 2025-06-09T10:09:30.123Z
34+
// ...
35+
}
36+
```
37+
1138
# How to add to your project
1239
## Package dependencies
1340
```swift

Sources/AppStoreConnectWebhook/AppStoreConnectWebhook.swift

Lines changed: 0 additions & 8 deletions
This file was deleted.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//
2+
// AppStoreConnectWebhookPayload.swift
3+
// AppStoreConnectWebhook
4+
//
5+
// Created by treastrain on 2025/06/20.
6+
//
7+
8+
public import Foundation
9+
10+
public struct AppStoreConnectWebhookPayload: Codable, Sendable {
11+
public let data: AppStoreConnectWebhookPayloadData
12+
13+
public init(data: AppStoreConnectWebhookPayloadData) {
14+
self.data = data
15+
}
16+
}
17+
18+
extension AppStoreConnectWebhookPayload {
19+
public init(json: String, using encoding: String.Encoding = .utf8) throws {
20+
let data = json.data(using: encoding)!
21+
let decoder = JSONDecoder()
22+
decoder.dateDecodingStrategy = .iso8601IncludingFractionalSeconds
23+
self = try decoder.decode(Self.self, from: data)
24+
}
25+
}
26+
27+
extension AppStoreConnectWebhookPayload: CustomStringConvertible {
28+
public var description: String { logDescription }
29+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//
2+
// AppStoreConnectWebhookPayloadData.swift
3+
// AppStoreConnectWebhook
4+
//
5+
// Created by treastrain on 2025/06/20.
6+
//
7+
8+
import Foundation
9+
10+
public enum AppStoreConnectWebhookPayloadData: Sendable {
11+
case webhookPingCreated(WebhookPingCreated)
12+
}
13+
14+
enum AppStoreConnectWebhookPayloadDataType: String, Codable, Sendable {
15+
case webhookPingCreated
16+
}
17+
18+
extension AppStoreConnectWebhookPayloadData: Codable {
19+
enum CodingKeys: CodingKey {
20+
case type
21+
}
22+
23+
public init(from decoder: any Decoder) throws {
24+
let container = try decoder.container(keyedBy: CodingKeys.self)
25+
guard container.allKeys.contains(.type) else {
26+
throw DecodingError.typeMismatch(
27+
AppStoreConnectWebhookPayloadData.self,
28+
DecodingError.Context.init(
29+
codingPath: container.codingPath,
30+
debugDescription: "Missing 'type' key in payload data.",
31+
underlyingError: nil
32+
)
33+
)
34+
}
35+
let type = try container.decode(
36+
AppStoreConnectWebhookPayloadDataType.self,
37+
forKey: .type
38+
)
39+
switch type {
40+
case .webhookPingCreated:
41+
let webhookPingCreated = try WebhookPingCreated(from: decoder)
42+
self = .webhookPingCreated(webhookPingCreated)
43+
}
44+
}
45+
46+
public func encode(to encoder: any Encoder) throws {
47+
var container = encoder.container(keyedBy: CodingKeys.self)
48+
switch self {
49+
case .webhookPingCreated(let webhookPingCreated):
50+
try container.encode(
51+
AppStoreConnectWebhookPayloadDataType.webhookPingCreated,
52+
forKey: .type
53+
)
54+
try webhookPingCreated.encode(to: encoder)
55+
}
56+
}
57+
}
58+
59+
extension AppStoreConnectWebhookPayloadData: CustomStringConvertible {
60+
public var description: String { logDescription }
61+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//
2+
// AppStoreConnectWebhookPayloadDataWebhookPingCreated.swift
3+
// AppStoreConnectWebhook
4+
//
5+
// Created by treastrain on 2025/06/20.
6+
//
7+
8+
public import Foundation
9+
10+
extension AppStoreConnectWebhookPayloadData {
11+
public struct WebhookPingCreated: Codable, Sendable {
12+
public struct Attributes: Codable, Sendable {
13+
public let timestamp: Date
14+
15+
public init(timestamp: Date) {
16+
self.timestamp = timestamp
17+
}
18+
}
19+
20+
public let type: String
21+
public let id: String
22+
public let version: Int
23+
public let attributes: Attributes
24+
25+
public init(
26+
type: String,
27+
id: String,
28+
version: Int,
29+
attributes: Attributes
30+
) {
31+
self.type = type
32+
self.id = id
33+
self.version = version
34+
self.attributes = attributes
35+
}
36+
}
37+
}
38+
39+
extension AppStoreConnectWebhookPayloadData.WebhookPingCreated:
40+
CustomStringConvertible
41+
{
42+
public var description: String { logDescription }
43+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//
2+
// DateCodingStrategy.swift
3+
// AppStoreConnectWebhook
4+
//
5+
// Created by treastrain on 2025/06/20.
6+
//
7+
8+
public import Foundation
9+
10+
private let style = Date.ISO8601FormatStyle()
11+
.year()
12+
.month()
13+
.day()
14+
.timeZone(separator: .omitted)
15+
.time(includingFractionalSeconds: true)
16+
.timeSeparator(.colon)
17+
18+
extension JSONDecoder.DateDecodingStrategy {
19+
public static var iso8601IncludingFractionalSeconds: Self {
20+
.custom {
21+
try style.parse(
22+
$0.singleValueContainer()
23+
.decode(String.self)
24+
)
25+
}
26+
}
27+
}
28+
29+
extension JSONEncoder.DateEncodingStrategy {
30+
public static var iso8601IncludingFractionalSeconds: Self {
31+
.custom {
32+
var container = $1.singleValueContainer()
33+
try container.encode(style.format($0))
34+
}
35+
}
36+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//
2+
// Encodable+.swift
3+
// AppStoreConnectWebhook
4+
//
5+
// Created by treastrain on 2025/06/20.
6+
//
7+
8+
import Foundation
9+
10+
private let logDescriptionEncoder = {
11+
let encoder = JSONEncoder()
12+
encoder.outputFormatting = [
13+
.prettyPrinted,
14+
.sortedKeys,
15+
.withoutEscapingSlashes,
16+
]
17+
encoder.dateEncodingStrategy = .iso8601IncludingFractionalSeconds
18+
return encoder
19+
}()
20+
21+
extension Encodable {
22+
var logDescription: String {
23+
let data = try! logDescriptionEncoder.encode(self)
24+
return String(data: data, encoding: .utf8)!
25+
}
26+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
//
2+
// AppStoreConnectWebhookPayloadDataWebhookPingCreatedTests.swift
3+
// AppStoreConnectWebhook
4+
//
5+
// Created by treastrain on 2025/06/20.
6+
//
7+
8+
import AppStoreConnectWebhook
9+
import Foundation
10+
import Testing
11+
12+
struct AppStoreConnectWebhookPayloadDataWebhookPingCreatedTests {
13+
@Test
14+
func decode() throws {
15+
let payloadJSON = """
16+
{
17+
"data": {
18+
"type": "webhookPingCreated",
19+
"id": "01234567-abcd-8901-dcba-987654321012",
20+
"version": 1,
21+
"attributes": {
22+
"timestamp": "2025-06-09T10:09:30.123456789Z"
23+
}
24+
}
25+
}
26+
"""
27+
let payload = try AppStoreConnectWebhookPayload(json: payloadJSON)
28+
guard case .webhookPingCreated(let webhookPingCreated) = payload.data
29+
else {
30+
Issue.record(
31+
"Expected payload data to be of type WebhookPingCreated, but got \(payload.data)"
32+
)
33+
return
34+
}
35+
36+
#expect(webhookPingCreated.type == "webhookPingCreated")
37+
#expect(webhookPingCreated.id == "01234567-abcd-8901-dcba-987654321012")
38+
#expect(webhookPingCreated.version == 1)
39+
#expect(
40+
webhookPingCreated.attributes.timestamp
41+
== Date(timeIntervalSinceReferenceDate: 771156570.123456789)
42+
)
43+
}
44+
45+
@Test
46+
func encode() throws {
47+
let encoder = JSONEncoder()
48+
encoder.dateEncodingStrategy = .iso8601IncludingFractionalSeconds
49+
encoder.outputFormatting = [.sortedKeys]
50+
51+
let payload = AppStoreConnectWebhookPayload(
52+
data: .webhookPingCreated(
53+
AppStoreConnectWebhookPayloadData.WebhookPingCreated(
54+
type: "webhookPingCreated",
55+
id: "01234567-abcd-8901-dcba-987654321012",
56+
version: 1,
57+
attributes: .init(
58+
timestamp: Date(
59+
timeIntervalSinceReferenceDate: 771156570.123456789
60+
)
61+
)
62+
)
63+
)
64+
)
65+
let payloadData = try encoder.encode(payload)
66+
let payloadJSON = String(data: payloadData, encoding: .utf8)!
67+
68+
#expect(
69+
payloadJSON
70+
== #"{"data":{"attributes":{"timestamp":"2025-06-09T10:09:30.123Z"},"id":"01234567-abcd-8901-dcba-987654321012","type":"webhookPingCreated","version":1}}"#
71+
)
72+
}
73+
}

Tests/AppStoreConnectWebhookTests/AppStoreConnectWebhookTests.swift

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

0 commit comments

Comments
 (0)