Skip to content

Commit 6d52c6d

Browse files
Basic support of custom post types (#25157)
* Create WordPressClientFactory * Instantiate WordPressApiCache and service in WordPressClient * Show custom post types * Extract `CustomPostCollectionView` * Support searching custom posts * Re-implement showing loading more states * Adopt new GBK API * Simple post conflict detection * Add a Filter type * Update wordpress-rs * Fix a compilation issue in unit tests * Add a missing delegate function * Improve excerpt rendering * Add a TODO * Use Picker to present the status filter * Use DDLog instead of NSLog * Extract data loading code to a view model type * Add a localizable string * Fix compilation issues after merge * Fix a typo Co-authored-by: Jeremy Massel <[email protected]> --------- Co-authored-by: Jeremy Massel <[email protected]>
1 parent 86c389f commit 6d52c6d

33 files changed

Lines changed: 1348 additions & 51 deletions
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import Foundation
2+
import WordPressAPI
3+
import WordPressAPIInternal
4+
import WordPressApiCache
5+
6+
extension WordPressApiCache {
7+
static func bootstrap() -> WordPressApiCache? {
8+
let instance: WordPressApiCache? = .onDiskCache() ?? .memoryCache()
9+
instance?.startListeningForUpdates()
10+
return instance
11+
}
12+
13+
private static func onDiskCache() -> WordPressApiCache? {
14+
let cacheURL: URL
15+
do {
16+
cacheURL = try FileManager.default
17+
.url(for: .libraryDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
18+
.appending(path: "app.sqlite")
19+
} catch {
20+
NSLog("Failed to create api cache file: \(error)")
21+
return nil
22+
}
23+
24+
if let cache = WordPressApiCache.onDiskCache(file: cacheURL) {
25+
return cache
26+
}
27+
28+
if FileManager.default.fileExists(at: cacheURL) {
29+
do {
30+
try FileManager.default.removeItem(at: cacheURL)
31+
32+
if let cache = WordPressApiCache.onDiskCache(file: cacheURL) {
33+
return cache
34+
}
35+
} catch {
36+
NSLog("Failed to delete sqlite database: \(error)")
37+
}
38+
}
39+
40+
return nil
41+
}
42+
43+
private static func onDiskCache(file: URL) -> WordPressApiCache? {
44+
let cache: WordPressApiCache
45+
do {
46+
cache = try WordPressApiCache(url: file)
47+
} catch {
48+
NSLog("Failed to create an instance: \(error)")
49+
return nil
50+
}
51+
52+
do {
53+
_ = try cache.performMigrations()
54+
} catch {
55+
NSLog("Failed to migrate database: \(error)")
56+
return nil
57+
}
58+
59+
return cache
60+
}
61+
62+
private static func memoryCache() -> WordPressApiCache? {
63+
do {
64+
let cache = try WordPressApiCache()
65+
_ = try cache.performMigrations()
66+
return cache
67+
} catch {
68+
NSLog("Failed to create memory cache: \(error)")
69+
return nil
70+
}
71+
}
72+
}

Modules/Sources/WordPressCore/Plugins/PluginService.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,9 @@ private extension PluginService {
181181

182182
func checkPluginUpdates(plugins: [PluginWithViewContext]) async throws {
183183
let updateCheck = try await wpOrgClient.checkPluginUpdates(
184-
// Use a fairely recent version if the actual version is unknown.
184+
// Use a fairly recent version if the actual version is unknown.
185185
wordpressCoreVersion: wordpressCoreVersion ?? "6.6",
186-
siteUrl: ParsedUrl.parse(input: client.rootUrl),
186+
siteUrl: ParsedUrl.parse(input: client.siteURL.absoluteString),
187187
plugins: plugins
188188
)
189189
let updateAvailable = updateCheck.plugins

Modules/Sources/WordPressCore/WordPressClient.swift

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Foundation
22
import WordPressAPI
33
import WordPressAPIInternal
4+
import WordPressApiCache
45

56
/// Protocol defining the WordPress API methods that WordPressClient needs.
67
/// This abstraction allows for mocking in tests using the `NoHandle` constructors
@@ -15,6 +16,9 @@ public protocol WordPressClientAPI: Sendable {
1516
var taxonomies: TaxonomiesRequestExecutor { get }
1617
var terms: TermsRequestExecutor { get }
1718
var applicationPasswords: ApplicationPasswordsRequestExecutor { get }
19+
var posts: PostsRequestExecutor { get }
20+
21+
func createSelfHostedService(cache: WordPressApiCache) throws -> WpSelfHostedService
1822

1923
func uploadMedia(
2024
params: MediaCreateParams,
@@ -70,11 +74,34 @@ public actor WordPressClient {
7074
case noActiveTheme
7175
}
7276

77+
public let siteURL: URL
78+
7379
/// The underlying API executor used for making network requests.
7480
public let api: any WordPressClientAPI
7581

76-
/// The root URL of the WordPress site this client is connected to.
77-
public let rootUrl: String
82+
private var _cache: WordPressApiCache?
83+
public var cache: WordPressApiCache? {
84+
get {
85+
if _cache == nil {
86+
_cache = WordPressApiCache.bootstrap()
87+
}
88+
return _cache
89+
}
90+
}
91+
92+
private var _service: WpSelfHostedService?
93+
public var service: WpSelfHostedService? {
94+
get {
95+
if _service == nil, let cache {
96+
do {
97+
_service = try api.createSelfHostedService(cache: cache)
98+
} catch {
99+
NSLog("Failed to create service: \(error)")
100+
}
101+
}
102+
return _service
103+
}
104+
}
78105

79106
/// The cached task for fetching site API details.
80107
private var loadSiteInfoTask: Task<WpApiDetails, Error>
@@ -93,10 +120,10 @@ public actor WordPressClient {
93120
///
94121
/// - Parameters:
95122
/// - api: The API executor to use for network requests.
96-
/// - rootUrl: The parsed root URL of the WordPress site.
97-
public init(api: any WordPressClientAPI, rootUrl: ParsedUrl) {
123+
/// - siteURL: The parsed root URL of the WordPress site.
124+
public init(api: WordPressClientAPI, siteURL: URL) {
98125
self.api = api
99-
self.rootUrl = rootUrl.url()
126+
self.siteURL = siteURL
100127

101128
// These tasks need to be manually restated here because we can't use the task constructors
102129
self.loadSiteInfoTask = Task { try await api.apiRoot.get().data }

Modules/Tests/WordPressCoreTests/MockWordPressClientAPI.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Foundation
22
import WordPressAPI
33
import WordPressAPIInternal
4+
import WordPressApiCache
45
@testable import WordPressCore
56

67
/// Tracks call counts for API methods to verify caching behavior.
@@ -60,6 +61,11 @@ final class MockWordPressClientAPI: WordPressClientAPI, @unchecked Sendable {
6061
var taxonomies: TaxonomiesRequestExecutor { fatalError("Not implemented") }
6162
var terms: TermsRequestExecutor { fatalError("Not implemented") }
6263
var applicationPasswords: ApplicationPasswordsRequestExecutor { fatalError("Not implemented") }
64+
var posts: PostsRequestExecutor { fatalError("Not implemented") }
65+
66+
func createSelfHostedService(cache: WordPressApiCache) throws -> WpSelfHostedService {
67+
fatalError("Not implemented")
68+
}
6369

6470
func uploadMedia(params: MediaCreateParams, fulfilling progress: Progress) async throws -> MediaRequestCreateResponse {
6571
fatalError("Not implemented")

Modules/Tests/WordPressCoreTests/WordPressClientFeatureTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ struct WordPressClientCachingTests {
2626
mockAPI.mockRoutes = ["/wp-block-editor/v1/settings"]
2727
mockAPI.mockIsBlockTheme = true
2828

29-
let client = try WordPressClient(api: mockAPI, rootUrl: .parse(input: "https://example.com"))
29+
let client = try WordPressClient(api: mockAPI, siteURL: URL(string: "https://example.com")!)
3030

3131
// First call - should trigger API fetches
3232
let result1 = try await client.supports(.blockEditorSettings)
@@ -61,7 +61,7 @@ struct WordPressClientCachingTests {
6161
let mockAPI = MockWordPressClientAPI()
6262
mockAPI.mockRoutes = ["/wp-block-editor/v1/sites/12345/settings"]
6363

64-
let client = try WordPressClient(api: mockAPI, rootUrl: .parse(input: "https://example.com"))
64+
let client = try WordPressClient(api: mockAPI, siteURL: URL(string: "https://example.com")!)
6565

6666
// Call with siteId
6767
let result = try await client.supports(.blockEditorSettings, forSiteId: 12345)
@@ -81,7 +81,7 @@ struct WordPressClientCachingTests {
8181
mockAPI.mockRoutes = ["/wp-block-editor/v1/settings", "/wp/v2/plugins"]
8282
mockAPI.mockIsBlockTheme = true
8383

84-
let client = try WordPressClient(api: mockAPI, rootUrl: .parse(input: "https://example.com"))
84+
let client = try WordPressClient(api: mockAPI, siteURL: URL(string: "https://example.com")!)
8585

8686
// Make multiple concurrent calls
8787
async let result1 = client.supports(.blockEditorSettings)

Sources/WordPressData/Swift/Blog+Plans.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,11 @@
1717
1031] // 2y Ecommerce Plan
1818
.contains(planID?.intValue)
1919
}
20+
21+
public var supportsCoreRESTAPI: Bool {
22+
if isHostedAtWPcom {
23+
return isAtomic()
24+
}
25+
return true
26+
}
2027
}

Sources/WordPressData/Swift/Blog+SelfHosted.swift

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -189,57 +189,68 @@ public extension WpApiApplicationPasswordDetails {
189189
}
190190
}
191191

192-
public enum WordPressSite {
193-
case dotCom(siteId: Int, authToken: String)
194-
case selfHosted(blogId: TaggedManagedObjectID<Blog>, apiRootURL: ParsedUrl, username: String, authToken: String)
192+
public enum WordPressSite: Hashable {
193+
case dotCom(siteURL: URL, siteId: Int, authToken: String)
194+
case selfHosted(blogId: TaggedManagedObjectID<Blog>, siteURL: URL, apiRootURL: ParsedUrl, username: String, authToken: String)
195195

196196
public init(blog: Blog) throws {
197+
let siteURL = try blog.getUrl()
197198
// Directly access the site content when available.
198199
if let restApiRootURL = blog.restApiRootURL,
199200
let restApiRootURL = try? ParsedUrl.parse(input: restApiRootURL),
200201
let username = blog.username,
201202
let authToken = try? blog.getApplicationToken() {
202-
self = .selfHosted(blogId: TaggedManagedObjectID(blog), apiRootURL: restApiRootURL, username: username, authToken: authToken)
203+
self = .selfHosted(blogId: TaggedManagedObjectID(blog), siteURL: siteURL, apiRootURL: restApiRootURL, username: username, authToken: authToken)
203204
} else if let account = blog.account, let siteId = blog.dotComID?.intValue {
204205
// When the site is added via a WP.com account, access the site via WP.com
205206
let authToken = try account.authToken ?? WPAccount.token(forUsername: account.username)
206-
self = .dotCom(siteId: siteId, authToken: authToken)
207+
self = .dotCom(siteURL: siteURL, siteId: siteId, authToken: authToken)
207208
} else {
208209
// In theory, this branch should never run, because the two if statements above should have covered all paths.
209210
// But we'll keep it here as the fallback.
210-
let url = try blog.restApiRootURL ?? blog.getUrl().appending(path: "wp-json").absoluteString
211-
let apiRootURL = try ParsedUrl.parse(input: url)
212-
self = .selfHosted(blogId: TaggedManagedObjectID(blog), apiRootURL: apiRootURL, username: try blog.getUsername(), authToken: try blog.getApplicationToken())
211+
let url = try blog.getUrl()
212+
let apiRootURL = try ParsedUrl.parse(input: blog.restApiRootURL ?? blog.getUrl().appending(path: "wp-json").absoluteString)
213+
self = .selfHosted(blogId: TaggedManagedObjectID(blog), siteURL: url, apiRootURL: apiRootURL, username: try blog.getUsername(), authToken: try blog.getApplicationToken())
214+
}
215+
}
216+
217+
public var siteURL: URL {
218+
switch self {
219+
case let .dotCom(siteURL, _, _):
220+
return siteURL
221+
case let .selfHosted(_, siteURL, _, _, _):
222+
return siteURL
213223
}
214224
}
215225

216226
public static func throughDotCom(blog: Blog) -> Self? {
217227
guard
228+
let siteURL = try? blog.getUrl(),
218229
let account = blog.account,
219230
let siteId = blog.dotComID?.intValue,
220231
let authToken = try? account.authToken ?? WPAccount.token(forUsername: account.username)
221232
else { return nil }
222233

223-
return .dotCom(siteId: siteId, authToken: authToken)
234+
return .dotCom(siteURL: siteURL, siteId: siteId, authToken: authToken)
224235
}
225236

226237
public func blog(in context: NSManagedObjectContext) throws -> Blog? {
227238
switch self {
228-
case let .dotCom(siteId, _):
239+
case let .dotCom(_, siteId, _):
229240
return try Blog.lookup(withID: siteId, in: context)
230-
case let .selfHosted(blogId, _, _, _):
241+
case let .selfHosted(blogId, _, _, _, _):
231242
return try context.existingObject(with: blogId)
232243
}
233244
}
234245

235246
public func blogId(in coreDataStack: CoreDataStack) -> TaggedManagedObjectID<Blog>? {
236247
switch self {
237-
case let .dotCom(siteId, _):
248+
case let .dotCom(_, siteId, _):
238249
return coreDataStack.performQuery { context in
239250
guard let blog = try? Blog.lookup(withID: siteId, in: context) else { return nil }
240251
return TaggedManagedObjectID(blog)
241252
}
242-
case let .selfHosted(id, _, _, _):
253+
case let .selfHosted(id, _, _, _, _):
243254
return id
244255
}
245256
}

Tests/KeystoneTests/Tests/Services/UserListViewModelTests.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ class UserListViewModelTests: XCTestCase {
1717
try await super.setUp()
1818

1919
let api = try WordPressAPI(urlSession: .shared, apiRootUrl: .parse(input: "https://example.com/wp-json"), authentication: .none)
20-
let client = try WordPressClient(api: api, rootUrl: .parse(input: "https://example.com"))
20+
let client = try WordPressClient(
21+
api: api,
22+
siteURL: URL(string: "https://example.com")!
23+
)
2124
service = UserService(client: client)
2225
viewModel = await UserListViewModel(userService: service, currentUserId: 0)
2326
}

WordPress/Classes/Login/ApplicationPasswordRequiredView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ struct ApplicationPasswordRequiredView<Content: View>: View {
2828
} else if showLoading {
2929
ProgressView()
3030
} else if let site {
31-
builder(WordPressClient(site: site))
31+
builder(WordPressClientFactory.shared.instance(for: site))
3232
} else {
3333
RestApiUpgradePrompt(localizedFeatureName: localizedFeatureName) {
3434
Task {

0 commit comments

Comments
 (0)