Skip to content
This repository was archived by the owner on Sep 15, 2025. It is now read-only.

Commit 0454d46

Browse files
committed
Safer decoding
1 parent f20b956 commit 0454d46

File tree

6 files changed

+101
-43
lines changed

6 files changed

+101
-43
lines changed

Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ let package = Package(
1111
targets: [
1212
.binaryTarget(
1313
name: "WordPressKit",
14-
url: "https://github.com/user-attachments/files/20123946/WordPressKit.zip",
15-
checksum: "e7905c7d063682c3a3433b4b36578169081c74895db02ec55ec8de2745c799ef"
14+
url: "https://github.com/user-attachments/files/20124623/WordPressKit.zip",
15+
checksum: "fb3d6043a07ffe1ba50bbbf3ca8daaa001bff7f05273bad2a113c89705400b1b"
1616
),
1717
]
1818
)

Sources/WordPressKit/Services/SubscribersServiceRemote.swift

Lines changed: 38 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,16 @@ public class SubscribersServiceRemote: ServiceRemoteWordPressComREST {
6363
public let isEmailSubscriptionEnabled: Bool
6464
public let subscriptionStatus: String?
6565

66-
private enum CodingKeys: String, CodingKey {
67-
case subscriberID = "subscription_id"
68-
case dotComUserID = "user_id"
69-
case displayName = "display_name"
70-
case emailAddress = "email_address"
71-
case avatar
72-
case dateSubscribed = "date_subscribed"
73-
case isEmailSubscriptionEnabled = "is_email_subscriber"
74-
case subscriptionStatus = "subscription_status"
66+
public init(from decoder: any Decoder) throws {
67+
let container = try decoder.container(keyedBy: StringCodingKey.self)
68+
subscriberID = try container.decode(Int.self, forKey: "subscription_id")
69+
dotComUserID = try container.decode(Int.self, forKey: "user_id")
70+
displayName = try? container.decodeIfPresent(String.self, forKey: "display_name")
71+
avatar = try? container.decodeIfPresent(String.self, forKey: "avatar")
72+
emailAddress = try? container.decodeIfPresent(String.self, forKey: "email_address")
73+
dateSubscribed = try container.decode(Date.self, forKey: "date_subscribed")
74+
isEmailSubscriptionEnabled = try container.decode(Bool.self, forKey: "is_email_subscriber")
75+
subscriptionStatus = try? container.decodeIfPresent(String.self, forKey: "subscription_status")
7576
}
7677
}
7778
}
@@ -129,7 +130,7 @@ public class SubscribersServiceRemote: ServiceRemoteWordPressComREST {
129130
var dateSubscribed: Date { get }
130131
}
131132

132-
public struct GetSubscriberDetailsResponse: Decodable, SubsciberBasicInfoResponse {
133+
public final class GetSubscriberDetailsResponse: Decodable, SubsciberBasicInfoResponse {
133134
public let subscriberID: Int
134135
public let dotComUserID: Int
135136
public let displayName: String?
@@ -140,7 +141,7 @@ public class SubscribersServiceRemote: ServiceRemoteWordPressComREST {
140141
public let isEmailSubscriptionEnabled: Bool
141142
public let subscriptionStatus: String?
142143
public let country: Country?
143-
public var plans: [Plan]?
144+
public let plans: [Plan]?
144145

145146
public struct Country: Decodable {
146147
public var code: String?
@@ -160,33 +161,35 @@ public class SubscribersServiceRemote: ServiceRemoteWordPressComREST {
160161
public let startDate: Date
161162
public let endDate: Date
162163

163-
enum CodingKeys: String, CodingKey {
164-
case isGift = "is_gift"
165-
case giftId = "gift_id"
166-
case paidSubscriptionId = "paid_subscription_id"
167-
case status
168-
case title
169-
case currency
170-
case renewInterval = "renew_interval"
171-
case inactiveRenewInterval = "inactive_renew_interval"
172-
case renewalPrice = "renewal_price"
173-
case startDate = "start_date"
174-
case endDate = "end_date"
164+
public init(from decoder: Decoder) throws {
165+
let container = try decoder.container(keyedBy: StringCodingKey.self)
166+
isGift = try container.decode(Bool.self, forKey: "is_gift")
167+
giftId = try container.decodeIfPresent(Int.self, forKey: "gift_id")
168+
paidSubscriptionId = try container.decodeIfPresent(String.self, forKey: "paid_subscription_id")
169+
status = try container.decode(String.self, forKey: "status")
170+
title = try container.decode(String.self, forKey: "title")
171+
currency = try container.decodeIfPresent(String.self, forKey: "currency")
172+
renewInterval = try? container.decodeIfPresent(String.self, forKey: "renew_interval")
173+
inactiveRenewInterval = try? container.decodeIfPresent(String.self, forKey: "inactive_renew_interval")
174+
renewalPrice = try container.decode(Decimal.self, forKey: "renewal_price")
175+
startDate = try container.decode(Date.self, forKey: "start_date")
176+
endDate = try container.decode(Date.self, forKey: "end_date")
175177
}
176178
}
177179

178-
private enum CodingKeys: String, CodingKey {
179-
case subscriberID = "subscription_id"
180-
case dotComUserID = "user_id"
181-
case displayName = "display_name"
182-
case emailAddress = "email_address"
183-
case avatar
184-
case siteURL = "url"
185-
case dateSubscribed = "date_subscribed"
186-
case isEmailSubscriptionEnabled = "is_email_subscriber"
187-
case subscriptionStatus = "subscription_status"
188-
case country
189-
case plans
180+
public init(from decoder: any Decoder) throws {
181+
let container = try decoder.container(keyedBy: StringCodingKey.self)
182+
subscriberID = try container.decode(Int.self, forKey: "subscription_id")
183+
dotComUserID = try container.decode(Int.self, forKey: "user_id")
184+
displayName = try? container.decodeIfPresent(String.self, forKey: "display_name")
185+
avatar = try? container.decodeIfPresent(String.self, forKey: "avatar")
186+
emailAddress = try? container.decodeIfPresent(String.self, forKey: "email_address")
187+
siteURL = try? container.decodeIfPresent(String.self, forKey: "url")
188+
dateSubscribed = try container.decode(Date.self, forKey: "date_subscribed")
189+
isEmailSubscriptionEnabled = try container.decode(Bool.self, forKey: "is_email_subscriber")
190+
subscriptionStatus = try? container.decodeIfPresent(String.self, forKey: "subscription_status")
191+
country = try? container.decodeIfPresent(Country.self, forKey: "country")
192+
plans = try container.decodeIfPresent([Plan].self, forKey: "plans")
190193
}
191194
}
192195

@@ -241,12 +244,6 @@ public class SubscribersServiceRemote: ServiceRemoteWordPressComREST {
241244
type: GetSubscriberStatsResponse.self
242245
).get().body
243246
}
244-
245-
// MARK: POST Delete Subscriber
246-
247-
public func deleteSubscriber() {
248-
249-
}
250247
}
251248

252249
extension SubscribersServiceRemote.SubsciberBasicInfoResponse {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import Foundation
2+
3+
struct StringCodingKey: CodingKey, ExpressibleByStringLiteral {
4+
private let string: String
5+
private var int: Int?
6+
7+
var stringValue: String { return string }
8+
9+
init(string: String) {
10+
self.string = string
11+
}
12+
13+
init?(stringValue: String) {
14+
self.string = stringValue
15+
}
16+
17+
var intValue: Int? { return int }
18+
19+
init?(intValue: Int) {
20+
self.string = String(describing: intValue)
21+
self.int = intValue
22+
}
23+
24+
init(stringLiteral value: String) {
25+
self.string = value
26+
}
27+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"user_id": 255064965,
3+
"subscription_id": 907116368,
4+
"email_address": "[email protected]",
5+
"date_subscribed": "2025-04-17T14:40:00+00:00",
6+
"is_email_subscriber": false,
7+
"subscription_status": "Subscribed",
8+
"avatar": "https://0.gravatar.com/avatar/694664524f7d391c4425ab07627f4e44e970f597985d24ce3dc4c27173316c20?s=128&d=https%3A%2F%2F0.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D128&r=G",
9+
"display_name": "Alex",
10+
"url": "http://test841027.wordpress.com",
11+
"country": {
12+
"code": "",
13+
"name": false
14+
}
15+
}

Tests/WordPressKitTests/Tests/SubscribersServiceRemoteTests.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,17 @@ class SubscribersServiceRemoteTests: RemoteTestCase, RESTTestable {
3434
XCTAssertEqual(plan.paidSubscriptionId, "12422686")
3535
}
3636

37+
func testDecoderSubscriberDetailsInvalidCountry() throws {
38+
let data = try JSONLoader.data(named: "site-subscriber-get-details-response-invalid-country")
39+
40+
let decoder = JSONDecoder()
41+
decoder.dateDecodingStrategy = JSONDecoder.DateDecodingStrategy.supportMultipleDateFormats
42+
43+
let response = try decoder.decode(SubscribersServiceRemote.GetSubscriberDetailsResponse.self, from: data)
44+
45+
XCTAssertNil(response.country)
46+
}
47+
3748
func testDecoderSubscriberStatsResponse() throws {
3849
let data = try JSONLoader.data(named: "site-subscriber-stats-response")
3950

WordPressKit.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
0CCD4C5C2C41700B00B53F9A /* UIDevice+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCD4C5B2C41700B00B53F9A /* UIDevice+Extensions.swift */; };
5252
0CCD4C5F2C41711800B53F9A /* NSObject-SafeExpectations in Frameworks */ = {isa = PBXBuildFile; productRef = 0CCD4C5E2C41711800B53F9A /* NSObject-SafeExpectations */; };
5353
0CCD4C622C41712800B53F9A /* wpxmlrpc in Frameworks */ = {isa = PBXBuildFile; productRef = 0CCD4C612C41712800B53F9A /* wpxmlrpc */; };
54+
0CD5D3DD2DCE4F5500B4E679 /* StringCodingKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD5D3DC2DCE4F5500B4E679 /* StringCodingKey.swift */; };
55+
0CD5D3DF2DCE50D900B4E679 /* site-subscriber-get-details-response-invalid-country.json in Resources */ = {isa = PBXBuildFile; fileRef = 0CD5D3DE2DCE50D900B4E679 /* site-subscriber-get-details-response-invalid-country.json */; };
5456
0CE311BD2DCBB52C003AADB3 /* SubscribersServiceRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE311BC2DCBB52C003AADB3 /* SubscribersServiceRemote.swift */; };
5557
0CE311BF2DCBB588003AADB3 /* SubscribersServiceRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE311BE2DCBB588003AADB3 /* SubscribersServiceRemoteTests.swift */; };
5658
0CE311C52DCBB970003AADB3 /* site-subscriber-stats-response.json in Resources */ = {isa = PBXBuildFile; fileRef = 0CE311C42DCBB970003AADB3 /* site-subscriber-stats-response.json */; };
@@ -830,6 +832,8 @@
830832
0CB1905F2A2A6943004D3E80 /* blaze-campaigns-search.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "blaze-campaigns-search.json"; sourceTree = "<group>"; };
831833
0CB190642A2A7569004D3E80 /* BlazeCampaignsSearchResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignsSearchResponse.swift; sourceTree = "<group>"; };
832834
0CCD4C5B2C41700B00B53F9A /* UIDevice+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+Extensions.swift"; sourceTree = "<group>"; };
835+
0CD5D3DC2DCE4F5500B4E679 /* StringCodingKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringCodingKey.swift; sourceTree = "<group>"; };
836+
0CD5D3DE2DCE50D900B4E679 /* site-subscriber-get-details-response-invalid-country.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "site-subscriber-get-details-response-invalid-country.json"; sourceTree = "<group>"; };
833837
0CE311BC2DCBB52C003AADB3 /* SubscribersServiceRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribersServiceRemote.swift; sourceTree = "<group>"; };
834838
0CE311BE2DCBB588003AADB3 /* SubscribersServiceRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribersServiceRemoteTests.swift; sourceTree = "<group>"; };
835839
0CE311C42DCBB970003AADB3 /* site-subscriber-stats-response.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "site-subscriber-stats-response.json"; sourceTree = "<group>"; };
@@ -2037,6 +2041,7 @@
20372041
3F3195AC266FF94B00397EE7 /* ZendeskMetadata.swift */,
20382042
4AE278432B2FAF6200E4D9B1 /* HTTPProtocolHelpers.swift */,
20392043
0CCD4C5B2C41700B00B53F9A /* UIDevice+Extensions.swift */,
2044+
0CD5D3DC2DCE4F5500B4E679 /* StringCodingKey.swift */,
20402045
);
20412046
path = Utility;
20422047
sourceTree = "<group>";
@@ -2556,6 +2561,7 @@
25562561
0C8069A62DC03E85008DFC2F /* site-subscribers-response.json */,
25572562
0CE311C42DCBB970003AADB3 /* site-subscriber-stats-response.json */,
25582563
0CE311C62DCBBA01003AADB3 /* site-subscriber-get-details-response.json */,
2564+
0CD5D3DE2DCE50D900B4E679 /* site-subscriber-get-details-response-invalid-country.json */,
25592565
74D67F0E1F15C2D70010C5ED /* site-roles-success.json */,
25602566
D8DB404121EF22B500B8238E /* site-segments-multiple.json */,
25612567
D813437721F6D7DC0060D99A /* site-segments-single.json */,
@@ -3082,6 +3088,7 @@
30823088
74C473CD1EF336BD009918F2 /* site-active-purchases-bad-json-failure.json in Resources */,
30833089
436D5645211B801100CEAA33 /* validate-domain-contact-information-response-success.json in Resources */,
30843090
FE5096652A309DEE00DDD071 /* jetpack-social-with-publicize.json in Resources */,
3091+
0CD5D3DF2DCE50D900B4E679 /* site-subscriber-get-details-response-invalid-country.json in Resources */,
30853092
74D67F351F15C3740010C5ED /* site-users-delete-not-member-failure.json in Resources */,
30863093
E1E89C681FD6B2E9006E7A33 /* plugin-directory-jetpack.json in Resources */,
30873094
74D67F201F15C3240010C5ED /* people-validate-invitation-failure.json in Resources */,
@@ -3471,6 +3478,7 @@
34713478
C7A09A52284104DB003096ED /* QRLoginServiceRemote.swift in Sources */,
34723479
4A68E3DD294070A7004AC3DC /* RemoteReaderSite.swift in Sources */,
34733480
40AB1ADA200FED25009B533D /* PluginDirectoryFeedPage.swift in Sources */,
3481+
0CD5D3DD2DCE4F5500B4E679 /* StringCodingKey.swift in Sources */,
34743482
436D56352118D85800CEAA33 /* WPCountry.swift in Sources */,
34753483
74A44DCB1F13C533006CD8F4 /* NotificationSettingsServiceRemote.swift in Sources */,
34763484
FAD1344525908F5F00A8FEB1 /* JetpackBackupServiceRemote.swift in Sources */,

0 commit comments

Comments
 (0)