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

Commit d95c88b

Browse files
authored
Stats: Add stats/emails/summary endpoint support (#794)
2 parents 1c9aa7e + 78d3082 commit d95c88b

File tree

6 files changed

+222
-0
lines changed

6 files changed

+222
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ _None._
3636

3737
- Add `getPost(withID)` to `PostServiceRemoteExtended` [#785]
3838
- Add support for metadata to `PostServiceRemoteExtended` [#783]
39+
- Add fetching of `StatsEmailsSummaryData` to `StatsService` [#794]
3940

4041
### Bug Fixes
4142

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import Foundation
2+
import WordPressShared
3+
4+
public struct StatsEmailsSummaryData: Decodable, Equatable {
5+
public let posts: [Post]
6+
7+
public init(posts: [Post]) {
8+
self.posts = posts
9+
}
10+
11+
private enum CodingKeys: String, CodingKey {
12+
case posts = "posts"
13+
}
14+
15+
public init(from decoder: any Decoder) throws {
16+
let container = try decoder.container(keyedBy: CodingKeys.self)
17+
posts = try container.decode([Post].self, forKey: .posts)
18+
}
19+
20+
public struct Post: Codable, Equatable {
21+
public let id: Int
22+
public let link: URL
23+
public let date: Date
24+
public let title: String
25+
public let type: PostType
26+
public let opens: Int
27+
public let clicks: Int
28+
29+
public init(id: Int, link: URL, date: Date, title: String, type: PostType, opens: Int, clicks: Int) {
30+
self.id = id
31+
self.link = link
32+
self.date = date
33+
self.title = title
34+
self.type = type
35+
self.opens = opens
36+
self.clicks = clicks
37+
}
38+
39+
public enum PostType: String, Codable {
40+
case post = "post"
41+
}
42+
43+
private enum CodingKeys: String, CodingKey {
44+
case id = "id"
45+
case link = "href"
46+
case date = "date"
47+
case title = "title"
48+
case type = "type"
49+
case opens = "opens"
50+
case clicks = "clicks"
51+
}
52+
53+
public init(from decoder: any Decoder) throws {
54+
let container = try decoder.container(keyedBy: CodingKeys.self)
55+
id = try container.decode(Int.self, forKey: .id)
56+
link = try container.decode(URL.self, forKey: .link)
57+
title = (try? container.decodeIfPresent(String.self, forKey: .title)) ?? ""
58+
type = (try? container.decodeIfPresent(PostType.self, forKey: .type)) ?? .post
59+
opens = (try? container.decodeIfPresent(Int.self, forKey: .opens)) ?? 0
60+
clicks = (try? container.decodeIfPresent(Int.self, forKey: .clicks)) ?? 0
61+
self.date = try container.decode(Date.self, forKey: .date)
62+
}
63+
}
64+
}
65+
66+
extension StatsEmailsSummaryData {
67+
public static var pathComponent: String {
68+
return "stats/emails/summary"
69+
}
70+
71+
public init?(jsonDictionary: [String: AnyObject]) {
72+
do {
73+
let jsonData = try JSONSerialization.data(withJSONObject: jsonDictionary, options: [])
74+
let decoder = JSONDecoder.apiDecoder
75+
self = try decoder.decode(Self.self, from: jsonData)
76+
} catch {
77+
return nil
78+
}
79+
}
80+
81+
public static func queryProperties(quantity: Int, sortField: SortField, sortOrder: SortOrder) -> [String: String] {
82+
return ["quantity": String(quantity), "sort_field": sortField.rawValue, "sort_order": sortOrder.rawValue]
83+
}
84+
85+
public enum SortField: String {
86+
case opens = "opens"
87+
case postId = "post_id"
88+
}
89+
90+
public enum SortOrder: String {
91+
case descending = "desc"
92+
case ascending = "ASC"
93+
}
94+
}

Sources/WordPressKit/Services/StatsServiceRemoteV2.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,32 @@ private extension StatsServiceRemoteV2 {
316316
}
317317
}
318318

319+
// MARK: - Emails Summary
320+
321+
public extension StatsServiceRemoteV2 {
322+
func getData(quantity: Int,
323+
sortField: StatsEmailsSummaryData.SortField = .opens,
324+
sortOrder: StatsEmailsSummaryData.SortOrder = .descending,
325+
completion: @escaping ((Result<StatsEmailsSummaryData, Error>) -> Void)) {
326+
let pathComponent = StatsEmailsSummaryData.pathComponent
327+
let path = self.path(forEndpoint: "sites/\(siteID)/\(pathComponent)/", withVersion: ._1_1)
328+
let properties = StatsEmailsSummaryData.queryProperties(quantity: quantity, sortField: sortField, sortOrder: sortOrder) as [String: AnyObject]
329+
330+
wordPressComRESTAPI.get(path, parameters: properties, success: { (response, _) in
331+
guard let jsonResponse = response as? [String: AnyObject],
332+
let emailsSummaryData = StatsEmailsSummaryData(jsonDictionary: jsonResponse)
333+
else {
334+
completion(.failure(ResponseError.decodingFailure))
335+
return
336+
}
337+
338+
completion(.success(emailsSummaryData))
339+
}, failure: { (error, _) in
340+
completion(.failure(error))
341+
})
342+
}
343+
}
344+
319345
// This serves both as a way to get the query properties in a "nice" way,
320346
// but also as a way to narrow down the generic type in `getInsight(completion:)` method.
321347
public protocol StatsInsightData {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"posts": [
3+
{
4+
"id": 53978,
5+
"href": "https://www.test1.com",
6+
"date": "2023-12-29 16:02:54",
7+
"title": "A great testing post",
8+
"type": "post",
9+
"opens": 453192,
10+
"clicks": 2202
11+
},
12+
{
13+
"id": 52362,
14+
"href": "http://www.test2.com",
15+
"date": "2023-06-27 19:15:12",
16+
"title": "Hot Off the Press: A new testing post",
17+
"type": "post",
18+
"opens": 450055,
19+
"clicks": 5385
20+
},
21+
{
22+
"id": 54733,
23+
"href": "http://www.test3.com",
24+
"date": "2024-03-06 21:06:28",
25+
"title": "Case Study: A new testing post",
26+
"type": "post",
27+
"opens": 382335,
28+
"clicks": 4063
29+
},
30+
{
31+
"id": 53668,
32+
"href": "http://www.test4.com",
33+
"date": "2023-11-07 20:18:07",
34+
"title": "Test without opens and clicks",
35+
"type": "post"
36+
}
37+
]
38+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import XCTest
2+
@testable import WordPressKit
3+
4+
final class StatsEmailsSummaryDataTests: XCTestCase {
5+
func testEmailsSummaryDecoding() throws {
6+
let json = getJSON("stats-emails-summary")
7+
8+
let emailsSummary = StatsEmailsSummaryData(jsonDictionary: json)
9+
XCTAssertNotNil(emailsSummary, "StatsEmailsSummaryTimeIntervalData not decoded as expected")
10+
let post = emailsSummary!.posts[0]
11+
12+
XCTAssertEqual(emailsSummary?.posts.count, 4)
13+
XCTAssertEqual(post.link, URL(string: "https://www.test1.com"))
14+
XCTAssertEqual(post.title, "A great testing post")
15+
XCTAssertEqual(post.type, .post)
16+
XCTAssertEqual(post.clicks, 2202)
17+
XCTAssertEqual(post.opens, 453192)
18+
}
19+
}
20+
21+
private extension StatsEmailsSummaryDataTests {
22+
func getJSON(_ fileName: String) -> [String: AnyObject] {
23+
let path = Bundle(for: type(of: self)).path(forResource: fileName, ofType: "json")!
24+
let data = try! Data(contentsOf: URL(fileURLWithPath: path))
25+
return try! JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String: AnyObject]
26+
}
27+
}

WordPressKit.xcodeproj/project.pbxproj

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
01383F7F2BD5545B00496B76 /* StatsEmailsSummaryDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01383F7E2BD5545B00496B76 /* StatsEmailsSummaryDataTests.swift */; };
11+
01383F822BD556B100496B76 /* stats-emails-summary.json in Resources */ = {isa = PBXBuildFile; fileRef = 01383F802BD5549E00496B76 /* stats-emails-summary.json */; };
1012
01438D362B6A31540097D60A /* stats-visits-month-unit-week.json in Resources */ = {isa = PBXBuildFile; fileRef = 01438D342B6A2B2C0097D60A /* stats-visits-month-unit-week.json */; };
1113
01438D392B6A361B0097D60A /* stats-summary.json in Resources */ = {isa = PBXBuildFile; fileRef = 01438D372B6A35FB0097D60A /* stats-summary.json */; };
1214
01438D3B2B6A36BF0097D60A /* StatsTotalsSummaryData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01438D3A2B6A36BF0097D60A /* StatsTotalsSummaryData.swift */; };
1315
0152100C28EDA9E400DD6783 /* StatsAnnualAndMostPopularTimeInsightDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0152100B28EDA9E400DD6783 /* StatsAnnualAndMostPopularTimeInsightDecodingTests.swift */; };
16+
019C5B8B2BD59CE000A69DB0 /* StatsEmailsSummaryData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 019C5B892BD59CE000A69DB0 /* StatsEmailsSummaryData.swift */; };
1417
0847B92C2A4442730044D32F /* IPLocationRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0847B92B2A4442730044D32F /* IPLocationRemote.swift */; };
1518
08C7493E2A45EA11000DA0E2 /* IPLocationRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08C7493D2A45EA11000DA0E2 /* IPLocationRemoteTests.swift */; };
1619
0C1C08412B9CD79900E52F8C /* PostServiceRemoteExtended.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1C08402B9CD79900E52F8C /* PostServiceRemoteExtended.swift */; };
@@ -754,10 +757,13 @@
754757
/* End PBXContainerItemProxy section */
755758

756759
/* Begin PBXFileReference section */
760+
01383F7E2BD5545B00496B76 /* StatsEmailsSummaryDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsEmailsSummaryDataTests.swift; sourceTree = "<group>"; };
761+
01383F802BD5549E00496B76 /* stats-emails-summary.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "stats-emails-summary.json"; sourceTree = "<group>"; };
757762
01438D342B6A2B2C0097D60A /* stats-visits-month-unit-week.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "stats-visits-month-unit-week.json"; sourceTree = "<group>"; };
758763
01438D372B6A35FB0097D60A /* stats-summary.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "stats-summary.json"; sourceTree = "<group>"; };
759764
01438D3A2B6A36BF0097D60A /* StatsTotalsSummaryData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatsTotalsSummaryData.swift; sourceTree = "<group>"; };
760765
0152100B28EDA9E400DD6783 /* StatsAnnualAndMostPopularTimeInsightDecodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsAnnualAndMostPopularTimeInsightDecodingTests.swift; sourceTree = "<group>"; };
766+
019C5B892BD59CE000A69DB0 /* StatsEmailsSummaryData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatsEmailsSummaryData.swift; sourceTree = "<group>"; };
761767
0847B92B2A4442730044D32F /* IPLocationRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPLocationRemote.swift; sourceTree = "<group>"; };
762768
08C7493D2A45EA11000DA0E2 /* IPLocationRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPLocationRemoteTests.swift; sourceTree = "<group>"; };
763769
0C1C08402B9CD79900E52F8C /* PostServiceRemoteExtended.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostServiceRemoteExtended.swift; sourceTree = "<group>"; };
@@ -1523,6 +1529,29 @@
15231529
/* End PBXFrameworksBuildPhase section */
15241530

15251531
/* Begin PBXGroup section */
1532+
01383F7D2BD5542300496B76 /* TimeInterval */ = {
1533+
isa = PBXGroup;
1534+
children = (
1535+
);
1536+
path = TimeInterval;
1537+
sourceTree = "<group>";
1538+
};
1539+
019C5B882BD59C7800A69DB0 /* Emails */ = {
1540+
isa = PBXGroup;
1541+
children = (
1542+
01383F7E2BD5545B00496B76 /* StatsEmailsSummaryDataTests.swift */,
1543+
);
1544+
path = Emails;
1545+
sourceTree = "<group>";
1546+
};
1547+
019C5B8A2BD59CE000A69DB0 /* Emails */ = {
1548+
isa = PBXGroup;
1549+
children = (
1550+
019C5B892BD59CE000A69DB0 /* StatsEmailsSummaryData.swift */,
1551+
);
1552+
path = Emails;
1553+
sourceTree = "<group>";
1554+
};
15261555
3297E1DC2564649D00287D21 /* Scan */ = {
15271556
isa = PBXGroup;
15281557
children = (
@@ -1654,6 +1683,7 @@
16541683
3FE2E9362BB10EC7002CA2E1 /* Stats */ = {
16551684
isa = PBXGroup;
16561685
children = (
1686+
019C5B8A2BD59CE000A69DB0 /* Emails */,
16571687
404057C3221B30140060250C /* Time Interval */,
16581688
40414061220F9F2800CF7C5B /* Insights */,
16591689
40819782221F5C8200A298E4 /* StatsPostDetails.swift */,
@@ -2480,6 +2510,7 @@
24802510
4081977D221F269A00A298E4 /* stats-visits-month.json */,
24812511
01438D342B6A2B2C0097D60A /* stats-visits-month-unit-week.json */,
24822512
40819779221F153A00A298E4 /* stats-visits-week.json */,
2513+
01383F802BD5549E00496B76 /* stats-emails-summary.json */,
24832514
436D56392118DE3B00CEAA33 /* supported-countries-success.json */,
24842515
436D56522121F60400CEAA33 /* supported-states-empty.json */,
24852516
436D563D2118E34D00CEAA33 /* supported-states-success.json */,
@@ -2597,6 +2628,8 @@
25972628
F3FF8A1C279C86F600E5C90F /* V2 */ = {
25982629
isa = PBXGroup;
25992630
children = (
2631+
019C5B882BD59C7800A69DB0 /* Emails */,
2632+
01383F7D2BD5542300496B76 /* TimeInterval */,
26002633
F3FF8A1D279C86FE00E5C90F /* Insights */,
26012634
);
26022635
path = V2;
@@ -3146,6 +3179,7 @@
31463179
740B23E41F17FB4200067A2A /* xmlrpc-metaweblog-editpost-success.xml in Resources */,
31473180
7434E1DE1F17C3C900C40DDB /* site-users-update-role-unknown-user-failure.json in Resources */,
31483181
4081977B221F153B00A298E4 /* stats-visits-week.json in Resources */,
3182+
01383F822BD556B100496B76 /* stats-emails-summary.json in Resources */,
31493183
);
31503184
runOnlyForDeploymentPostprocessing = 0;
31513185
};
@@ -3296,6 +3330,7 @@
32963330
8BB66DB02523C181000B29DA /* ReaderPostServiceRemote+V2.swift in Sources */,
32973331
74E229501F1E741B0085F7F2 /* RemotePublicizeConnection.swift in Sources */,
32983332
40E7FEB722106A8D0032834E /* StatsCommentsInsight.swift in Sources */,
3333+
019C5B8B2BD59CE000A69DB0 /* StatsEmailsSummaryData.swift in Sources */,
32993334
9856BE962630B5C200C12FEB /* RemoteUser+Likes.swift in Sources */,
33003335
3FD634E52BC3A55F00CEDF5E /* WordPressOrgXMLRPCValidator.swift in Sources */,
33013336
3FE2E97B2BC3A332002CA2E1 /* WordPressAPIError+NSErrorBridge.swift in Sources */,
@@ -3551,6 +3586,7 @@
35513586
93188D221F2264E60028ED4D /* TaxonomyServiceRemoteRESTTests.m in Sources */,
35523587
F194E1232417ED9F00874408 /* AtomicAuthenticationServiceRemoteTests.swift in Sources */,
35533588
4AB6A3652B83191600769115 /* ReaderPostServiceRemote+FetchEndpointTests.swift in Sources */,
3589+
01383F7F2BD5545B00496B76 /* StatsEmailsSummaryDataTests.swift in Sources */,
35543590
9817D9D426BC8AF000ECBD8C /* CommentServiceRemoteXMLRPCTests.swift in Sources */,
35553591
74FC6F3B1F191BB400112505 /* NotificationSyncServiceRemoteTests.swift in Sources */,
35563592
731BA83821DECD97000FDFCD /* SiteCreationResponseDecodingTests.swift in Sources */,

0 commit comments

Comments
 (0)