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

Commit 32ab230

Browse files
authored
Extend RemotePostService with new create and patch endpoints (#743)
2 parents 755fb4a + 44309e7 commit 32ab230

13 files changed

+507
-8
lines changed

WordPressKit.xcodeproj/project.pbxproj

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
0152100C28EDA9E400DD6783 /* StatsAnnualAndMostPopularTimeInsightDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0152100B28EDA9E400DD6783 /* StatsAnnualAndMostPopularTimeInsightDecodingTests.swift */; };
1616
0847B92C2A4442730044D32F /* IPLocationRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0847B92B2A4442730044D32F /* IPLocationRemote.swift */; };
1717
08C7493E2A45EA11000DA0E2 /* IPLocationRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08C7493D2A45EA11000DA0E2 /* IPLocationRemoteTests.swift */; };
18+
0C1C08412B9CD79900E52F8C /* PostServiceRemoteExtended.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1C08402B9CD79900E52F8C /* PostServiceRemoteExtended.swift */; };
19+
0C1C08432B9CD8D200E52F8C /* PostServiceRemoteREST+Extended.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1C08422B9CD8D200E52F8C /* PostServiceRemoteREST+Extended.swift */; };
20+
0C1C08452B9CDB0B00E52F8C /* PostServiceRemoteXMLRPC+Extended.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1C08442B9CDB0B00E52F8C /* PostServiceRemoteXMLRPC+Extended.swift */; };
21+
0C9CD7992B9A107E0045BE03 /* RemotePostParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9CD7982B9A107E0045BE03 /* RemotePostParameters.swift */; };
1822
0CB1905E2A2A5E83004D3E80 /* BlazeCampaign.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB1905D2A2A5E83004D3E80 /* BlazeCampaign.swift */; };
1923
0CB190612A2A6A13004D3E80 /* blaze-campaigns-search.json in Resources */ = {isa = PBXBuildFile; fileRef = 0CB1905F2A2A6943004D3E80 /* blaze-campaigns-search.json */; };
2024
0CB190652A2A7569004D3E80 /* BlazeCampaignsSearchResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB190642A2A7569004D3E80 /* BlazeCampaignsSearchResponse.swift */; };
@@ -737,7 +741,11 @@
737741
0152100B28EDA9E400DD6783 /* StatsAnnualAndMostPopularTimeInsightDecodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsAnnualAndMostPopularTimeInsightDecodingTests.swift; sourceTree = "<group>"; };
738742
0847B92B2A4442730044D32F /* IPLocationRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPLocationRemote.swift; sourceTree = "<group>"; };
739743
08C7493D2A45EA11000DA0E2 /* IPLocationRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPLocationRemoteTests.swift; sourceTree = "<group>"; };
744+
0C1C08402B9CD79900E52F8C /* PostServiceRemoteExtended.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostServiceRemoteExtended.swift; sourceTree = "<group>"; };
745+
0C1C08422B9CD8D200E52F8C /* PostServiceRemoteREST+Extended.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PostServiceRemoteREST+Extended.swift"; sourceTree = "<group>"; };
746+
0C1C08442B9CDB0B00E52F8C /* PostServiceRemoteXMLRPC+Extended.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PostServiceRemoteXMLRPC+Extended.swift"; sourceTree = "<group>"; };
740747
0C3A2A412A2E7BA500FD91D6 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = "<group>"; };
748+
0C9CD7982B9A107E0045BE03 /* RemotePostParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemotePostParameters.swift; sourceTree = "<group>"; };
741749
0CB1905D2A2A5E83004D3E80 /* BlazeCampaign.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaign.swift; sourceTree = "<group>"; };
742750
0CB1905F2A2A6943004D3E80 /* blaze-campaigns-search.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "blaze-campaigns-search.json"; sourceTree = "<group>"; };
743751
0CB190642A2A7569004D3E80 /* BlazeCampaignsSearchResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignsSearchResponse.swift; sourceTree = "<group>"; };
@@ -2026,12 +2034,15 @@
20262034
E1BD95141FD5A2B800CD5CE3 /* PluginDirectoryServiceRemote.swift */,
20272035
E13EE1461F33258E00C15787 /* PluginServiceRemote.swift */,
20282036
740B23B21F17EC7300067A2A /* PostServiceRemote.h */,
2037+
0C1C08402B9CD79900E52F8C /* PostServiceRemoteExtended.swift */,
20292038
740B23BC1F17ECB500067A2A /* PostServiceRemoteOptions.h */,
20302039
740B23B31F17EC7300067A2A /* PostServiceRemoteREST.h */,
20312040
740B23B41F17EC7300067A2A /* PostServiceRemoteREST.m */,
2041+
0C1C08422B9CD8D200E52F8C /* PostServiceRemoteREST+Extended.swift */,
20322042
9AF4F2FB218331DC00570E4B /* PostServiceRemoteREST+Revisions.swift */,
20332043
740B23B51F17EC7300067A2A /* PostServiceRemoteXMLRPC.h */,
20342044
740B23B61F17EC7300067A2A /* PostServiceRemoteXMLRPC.m */,
2045+
0C1C08442B9CDB0B00E52F8C /* PostServiceRemoteXMLRPC+Extended.swift */,
20352046
F181EA0127184D3C00F26141 /* ProductServiceRemote.swift */,
20362047
74A44DCA1F13C533006CD8F4 /* PushAuthenticationServiceRemote.swift */,
20372048
C7A09A4F284104DB003096ED /* QR Login */,
@@ -2129,6 +2140,7 @@
21292140
E61A51A521B172A900A5F902 /* RemoteWpcomPlan.swift */,
21302141
740B23C01F17EE8000067A2A /* RemotePost.h */,
21312142
740B23C11F17EE8000067A2A /* RemotePost.m */,
2143+
0C9CD7982B9A107E0045BE03 /* RemotePostParameters.swift */,
21322144
740B23BE1F17EE8000067A2A /* RemotePostCategory.h */,
21332145
740B23BF1F17EE8000067A2A /* RemotePostCategory.m */,
21342146
93188D1C1F2262BF0028ED4D /* RemotePostTag.h */,
@@ -3494,14 +3506,17 @@
34943506
436D56352118D85800CEAA33 /* WPCountry.swift in Sources */,
34953507
74A44DCB1F13C533006CD8F4 /* NotificationSettingsServiceRemote.swift in Sources */,
34963508
FAD1344525908F5F00A8FEB1 /* JetpackBackupServiceRemote.swift in Sources */,
3509+
0C1C08452B9CDB0B00E52F8C /* PostServiceRemoteXMLRPC+Extended.swift in Sources */,
34973510
F1BB7806240FB90B0030ADDC /* AtomicAuthenticationServiceRemote.swift in Sources */,
34983511
404057CE221C38130060250C /* StatsTopVideosTimeIntervalData.swift in Sources */,
34993512
7E0D64FF22D855700092AD10 /* EditorServiceRemote.swift in Sources */,
3513+
0C1C08412B9CD79900E52F8C /* PostServiceRemoteExtended.swift in Sources */,
35003514
9AF4F2FF2183346B00570E4B /* RemoteRevision.swift in Sources */,
35013515
17D936252475D8AB008B2205 /* RemoteHomepageType.swift in Sources */,
35023516
74BA04F41F06DC0A00ED5CD8 /* CommentServiceRemoteREST.m in Sources */,
35033517
74C473AC1EF2F75E009918F2 /* SiteManagementServiceRemote.swift in Sources */,
35043518
74585B971F0D54B400E7E667 /* RemoteDomain.swift in Sources */,
3519+
0C1C08432B9CD8D200E52F8C /* PostServiceRemoteREST+Extended.swift in Sources */,
35053520
74A44DD01F13C64B006CD8F4 /* RemoteNotification.swift in Sources */,
35063521
8B52B901257AC5A200221663 /* Date+endOfDay.swift in Sources */,
35073522
E1D6B558200E473A00325669 /* TimeZoneServiceRemote.swift in Sources */,
@@ -3517,6 +3532,7 @@
35173532
93BD27811EE73944002BB00B /* WordPressOrgXMLRPCApi.swift in Sources */,
35183533
4A57A6832B54A326008D0660 /* WordPressAPIError+NSErrorBrdige.swift in Sources */,
35193534
439A44D62107C66A00795ED7 /* JSONDecoderExtension.swift in Sources */,
3535+
0C9CD7992B9A107E0045BE03 /* RemotePostParameters.swift in Sources */,
35203536
B5A4822B20AC6C0B009D95F6 /* WPKitLogging.swift in Sources */,
35213537
B5A4822E20AC6C1A009D95F6 /* WPKitLogging.m in Sources */,
35223538
7430C9A61F1927180051B8E6 /* ReaderSiteServiceRemote.m in Sources */,

WordPressKit/NSDate+WordPressJSON.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
@interface NSDate (WordPressJSON)
44

5+
+ (NSDateFormatter *)rfc3339DateFormatter;
6+
57
/**
68
Parses a date string
79

WordPressKit/PostServiceRemote.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#import <WordPressKit/PostServiceRemoteOptions.h>
33

44
@class RemotePost;
5+
@class RemotePostUpdateParameters;
56

67
@protocol PostServiceRemote <NSObject>
78

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import Foundation
2+
3+
public protocol PostServiceRemoteExtended: PostServiceRemote {
4+
/// Creates a new post with the given parameters.
5+
func createPost(with parameters: RemotePostCreateParameters) async throws -> RemotePost
6+
7+
/// Performs a partial update to the existing post.
8+
///
9+
/// - throws: ``PostServiceRemoteUpdatePostError`` or oher underlying errors
10+
/// (see ``WordPressAPIError``)
11+
func patchPost(withID postID: Int, parameters: RemotePostUpdateParameters) async throws -> RemotePost
12+
}
13+
14+
public enum PostServiceRemoteUpdatePostError: Error {
15+
/// 409 (Conflict)
16+
case conflict
17+
/// 404 (Not Found)
18+
case notFound
19+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import Foundation
2+
3+
extension PostServiceRemoteREST: PostServiceRemoteExtended {
4+
public func createPost(with parameters: RemotePostCreateParameters) async throws -> RemotePost {
5+
let path = self.path(forEndpoint: "sites/\(siteID)/posts/new?context=edit", withVersion: ._1_2)
6+
let parameters = try makeParameters(from: RemotePostCreateParametersWordPressComEncoder(parameters: parameters))
7+
8+
let response = try await wordPressComRestApi.perform(.post, URLString: path, parameters: parameters).get()
9+
return try await decodePost(from: response.body)
10+
}
11+
12+
public func patchPost(withID postID: Int, parameters: RemotePostUpdateParameters) async throws -> RemotePost {
13+
let path = self.path(forEndpoint: "sites/\(siteID)/posts/\(postID)?context=edit", withVersion: ._1_2)
14+
let parameters = try makeParameters(from: RemotePostUpdateParametersWordPressComEncoder(parameters: parameters))
15+
16+
let result = await wordPressComRestApi.perform(.post, URLString: path, parameters: parameters)
17+
switch result {
18+
case .success(let response):
19+
return try await decodePost(from: response.body)
20+
case .failure(let error):
21+
guard case .endpointError(let error) = error else {
22+
throw error
23+
}
24+
switch error.apiErrorCode ?? "" {
25+
case "unknown_post": throw PostServiceRemoteUpdatePostError.notFound
26+
case "old-revision": throw PostServiceRemoteUpdatePostError.conflict
27+
default: throw error
28+
}
29+
}
30+
}
31+
}
32+
33+
// Decodes the post in the background.
34+
private func decodePost(from object: AnyObject) async throws -> RemotePost {
35+
guard let dictionary = object as? [AnyHashable: Any] else {
36+
throw WordPressAPIError<WordPressComRestApiEndpointError>.unparsableResponse(response: nil, body: nil)
37+
}
38+
return PostServiceRemoteREST.remotePost(fromJSONDictionary: dictionary)
39+
}
40+
41+
private func makeParameters<T: Encodable>(from value: T) throws -> [String: AnyObject] {
42+
let encoder = JSONEncoder()
43+
encoder.dateEncodingStrategy = .formatted(NSDate.rfc3339DateFormatter())
44+
let data = try encoder.encode(value)
45+
let object = try JSONSerialization.jsonObject(with: data)
46+
guard let dictionary = object as? [String: AnyObject] else {
47+
throw URLError(.unknown) // This should never happen
48+
}
49+
return dictionary
50+
}

WordPressKit/PostServiceRemoteREST.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,7 @@
7676
success:(void (^ _Nullable)(NSArray<RemoteLikeUser *> * _Nonnull users, NSNumber * _Nonnull found))success
7777
failure:(void (^ _Nullable)(NSError * _Nullable))failure;
7878

79+
/// Returns a remote post with the given data.
80+
+ (nonnull RemotePost *)remotePostFromJSONDictionary:(nonnull NSDictionary *)jsonPost;
81+
7982
@end

WordPressKit/PostServiceRemoteREST.m

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,10 @@ - (NSArray *)remotePostsFromJSONArray:(NSArray *)jsonPosts {
435435
}
436436

437437
- (RemotePost *)remotePostFromJSONDictionary:(NSDictionary *)jsonPost {
438+
return [PostServiceRemoteREST remotePostFromJSONDictionary:jsonPost];
439+
}
440+
441+
+ (RemotePost *)remotePostFromJSONDictionary:(NSDictionary *)jsonPost {
438442
RemotePost *post = [RemotePost new];
439443
post.postID = jsonPost[@"ID"];
440444
post.siteID = jsonPost[@"site_ID"];
@@ -611,13 +615,13 @@ - (NSArray *)metadataForPost:(RemotePost *)post {
611615
}];
612616
}
613617

614-
- (NSArray *)remoteCategoriesFromJSONArray:(NSArray *)jsonCategories {
618+
+ (NSArray *)remoteCategoriesFromJSONArray:(NSArray *)jsonCategories {
615619
return [jsonCategories wp_map:^id(NSDictionary *jsonCategory) {
616620
return [self remoteCategoryFromJSONDictionary:jsonCategory];
617621
}];
618622
}
619623

620-
- (RemotePostCategory *)remoteCategoryFromJSONDictionary:(NSDictionary *)jsonCategory {
624+
+ (RemotePostCategory *)remoteCategoryFromJSONDictionary:(NSDictionary *)jsonCategory {
621625
RemotePostCategory *category = [RemotePostCategory new];
622626
category.categoryID = jsonCategory[@"ID"];
623627
category.name = jsonCategory[@"name"];
@@ -626,7 +630,7 @@ - (RemotePostCategory *)remoteCategoryFromJSONDictionary:(NSDictionary *)jsonCat
626630
return category;
627631
}
628632

629-
- (NSArray *)tagNamesFromJSONDictionary:(NSDictionary *)jsonTags {
633+
+ (NSArray *)tagNamesFromJSONDictionary:(NSDictionary *)jsonTags {
630634
return [jsonTags allKeys];
631635
}
632636

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import Foundation
2+
import wpxmlrpc
3+
4+
extension PostServiceRemoteXMLRPC: PostServiceRemoteExtended {
5+
public func createPost(with parameters: RemotePostCreateParameters) async throws -> RemotePost {
6+
let dictionary = try makeParameters(from: RemotePostCreateParametersXMLRPCEncoder(parameters: parameters, type: .post))
7+
let parameters = xmlrpcArguments(withExtra: dictionary) as [AnyObject]
8+
let response = try await api.call(method: "metaWeblog.newPost", parameters: parameters).get()
9+
guard let postID = (response.body as? NSNumber) else {
10+
throw URLError(.unknown) // Should never happen
11+
}
12+
return try await getPost(withID: postID)
13+
}
14+
15+
public func patchPost(withID postID: Int, parameters: RemotePostUpdateParameters) async throws -> RemotePost {
16+
let dictionary = try makeParameters(from: RemotePostUpdateParametersXMLRPCEncoder(parameters: parameters, type: .post))
17+
var parameters = xmlrpcArguments(withExtra: dictionary) as [AnyObject]
18+
if parameters.count > 0 {
19+
parameters[0] = postID as NSNumber
20+
}
21+
let result = await api.call(method: "metaWeblog.editPost", parameters: parameters)
22+
switch result {
23+
case .success:
24+
return try await getPost(withID: postID as NSNumber)
25+
case .failure(let error):
26+
guard case .endpointError(let error) = error else {
27+
throw error
28+
}
29+
switch error.code ?? 0 {
30+
case 404: throw PostServiceRemoteUpdatePostError.notFound
31+
case 409: throw PostServiceRemoteUpdatePostError.conflict
32+
default: throw error
33+
}
34+
}
35+
}
36+
37+
private func getPost(withID postID: NSNumber) async throws -> RemotePost {
38+
try await withUnsafeThrowingContinuation { continuation in
39+
getPostWithID(postID) { post in
40+
guard let post else {
41+
return continuation.resume(throwing: URLError(.unknown)) // Should never happen
42+
}
43+
continuation.resume(returning: post)
44+
} failure: { error in
45+
continuation.resume(throwing: error ?? URLError(.unknown))
46+
}
47+
}
48+
}
49+
}
50+
51+
private func makeParameters<T: Encodable>(from value: T) throws -> [String: AnyObject] {
52+
let encoder = PropertyListEncoder()
53+
encoder.outputFormat = .xml
54+
let data = try encoder.encode(value)
55+
let object = try PropertyListSerialization.propertyList(from: data, format: nil)
56+
guard let dictionary = object as? [String: AnyObject] else {
57+
throw URLError(.unknown) // This should never happen
58+
}
59+
return dictionary
60+
}

WordPressKit/PostServiceRemoteXMLRPC.m

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,6 @@ - (NSDictionary *)parametersWithRemotePost:(RemotePost *)post
406406
// Pass the current date so the post is updated correctly
407407
postParams[@"date_created_gmt"] = [NSDate date];
408408
}
409-
410409
if (post.categories) {
411410
NSArray *categoryNames = [post.categories wp_map:^id(RemotePostCategory *category) {
412411
return category.name;

WordPressKit/RemotePost.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ extern NSString * const PostStatusScheduled;
99
extern NSString * const PostStatusTrash;
1010
extern NSString * const PostStatusDeleted;
1111

12+
/// Represents the response object for APIs that create or update posts.
1213
@interface RemotePost : NSObject
1314
- (id)initWithSiteID:(NSNumber *)siteID status:(NSString *)status title:(NSString *)title content:(NSString *)content;
1415

0 commit comments

Comments
 (0)