-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rewrite WordPressOrgRestApi
#724
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO, forgetting to call that special function is an easy mistake to make during development.
As a forgetful developer, I can attest to that. Definitely best to remove the need for people to have to remember to do things and have the code and design either do that for them or force them to do them.
The problem with different path now becomes an internal implementation detail which callers don't need to worry about
Perfect 👌
None of my comments are blocking.
let remoteAPI: WordPressRestApi | ||
public init(remoteAPI: WordPressRestApi) { | ||
let remoteAPI: WordPressOrgRestApi | ||
public init(remoteAPI: WordPressOrgRestApi) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's a minor nitpick / curiosity. I have a thing for type names using acronyms, like URL
instead of Url
or JSONDecoder
instead of JsonDecoder
.
How would this look like if it were WordPressORGRESTAPI
? Too many uppercase letters? 😳
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd probably use Org
instead of ORG
. But this PR simply reuses the existing name 😄 .
/// Some may call API client using a string that contains path and query, like `api.get("post?id=1")`. This function | ||
/// can be used to support those use cases. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Formatting nitpick
/// Some may call API client using a string that contains path and query, like `api.get("post?id=1")`. This function | |
/// can be used to support those use cases. | |
/// Some may call API client using a string that contains path and query, like `api.get("post?id=1")`. | |
/// This function can be used to support those use cases. |
private func requestBuilder(URLString: String) throws -> HTTPRequestBuilder { | ||
guard let url = URL(string: URLString, relativeTo: baseURL) else { | ||
throw URLError(.badURL) | ||
} | ||
|
||
var builder = HTTPRequestBuilder(url: url) | ||
var builder = HTTPRequestBuilder(url: baseURL) | ||
.appendURLString(URLString) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
public var loginURL: URL | ||
public var username: String | ||
public var password: Secret<String> | ||
|
||
public var adminURL: URL |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do these need to be var
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
public var loginURL: URL | |
public var username: String | |
public var password: Secret<String> | |
public var adminURL: URL | |
public let loginURL: URL | |
public let username: String | |
public let password: Secret<String> | |
public let adminURL: URL |
public func get<Success: Decodable>( | ||
path: String, | ||
parameters: [String: Any]? = nil, | ||
jsonDecoder: JSONDecoder = JSONDecoder(), | ||
type: Success.Type = Success.self | ||
) async -> WordPressAPIResult<Success, WordPressOrgRestApiError> { | ||
await perform(.get, path: path, parameters: parameters, jsonDecoder: jsonDecoder, type: type) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inspired by try JSONDecoder().decode(Resource.self, from: jsonData)
, what do you think of moving type
at the start?
get(Post.type, path: "/post", parameters: nil)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like the decoding parameters are "secondary": The "GET /api/path" part is the main one. Plus, they have default values thus should be place at the end?
failure(error) | ||
} | ||
Task { @MainActor in | ||
await remote.perform(.put, path: path, parameters: parameters, type: AnyResponse.self) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you considered adding methods for PUT
and other HTTP verbs, for consistency with the POST
and GET
implementations?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, the AnyResponse
type and related .map { _ in }
took me a bit to understand.
I read them as a way to deal with an API which returns no data, or for which we don't care about the data.
What do you think of pushing this knowledge down at the WordPressOrgRestApi
level?
We could have a perform
implementation that returns WordPressAPIResult<Void, WordPressOrgRestApiError>
, allowing us to segregate the AnyResponse + .map { _ in }
"trick" lower into the implementation layer.
If instead it was implemented at this level because it's a one-off kind of thing, I'd still suggest centralizing the implementation into a fileprivate extension WordPressOrgRestApi
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you considered adding methods for PUT and other HTTP verbs, for consistency with the POST and GET implementations?
The post and get functions are convenience functions because the majority of HTTP requests uses these two methods. I didn't add convenience functions for other methods simply because they are not used by many places (maybe only one or two places?).
What do you think of pushing this knowledge down at the WordPressOrgRestApi level?
IMO, It's kind of strange to ignore the response. Adding such support to WordPressOrgRestApi
feels like we are encouraging it, but I don't think we should... Also, I believe this is the only place that ignores HTTP API response. I don't want to complicate WordPressOrgRestApi
too much for this one case.
case name = "name" | ||
case author = "author" | ||
case version = "version" | ||
case pluginURI = "plugin_uri" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's always one that gets in the way of the automatic key generation 😅
return HTTPStubsResponse(error: URLError(URLError.Code.networkConnectionLost)) | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe we should also remove the stubs?
override func tearDown() { | |
super.tearDown() | |
HTTPStubs.removeAllStubs() | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch! I always forget about it. Addressed in c44bb13.
func fetchBlockEditorSettings(completion: @escaping BlockEditorSettingsCompletionHandler) { | ||
Task { @MainActor in | ||
let result = await self.remoteAPI.get(path: "/wp-block-editor/v1/settings", parameters: ["context": "mobile"], type: RemoteBlockEditorSettings.self) | ||
.map { settings -> RemoteBlockEditorSettings? in settings } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might be a change for later on, but it seems like the only reason the RemoteBlockEditorSettings
the API call returns is wrapped in an Optional
here is because that's the type the "legacy" BlockEditorSettingsCompletionHandler
expects?
If so, and since we're making breaking changes, I wonder whether we could make the type in the completion typealias non-optional. It would remove this step, and I suspect also simplify caller code by removing the need to deal with Result
and Optional
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not just for fetchBlockEditorSettings
's syntax level backward compatibility, but most importantly runtime behaviour backward compatibility.
/wp-block-editor/v1/settings
endpoint returns a JSON that contains many properties. The fetchBlockEditorSettings
function only cares about a subset of these properties. See the model type RemoteBlockEditorSettings
, which contains mostly non-optional properties.
If the JSON response contains all the properties declared in the Swift type, this function of course should return a model instance.
However, if this API returns 200 with valid "block editor settings" JSON response, which does not contains all the non-optional proprieties declared in the RemoteBlockEditorSettings
type, the function should return a nil, instead of a "decoding error".
That's the behaviour of the existing function. And the refactored implementation (mainly the line here and the code below that maps decoding error to a nil successful result) maintains the same runtime behaviour for backward compatibility.
func testFetchBlockEditorSettingsNotThemeJSON() { | ||
stub(condition: isHost("public-api.wordpress.com") && isPath("/wp-block-editor/v1/sites/1/settings") && containsQueryParams(["context": "mobile"])) { _ in | ||
fixture(filePath: OHPathForFile(self.blockSettingsNOTThemeJSONResponseFilename, Self.self)!, headers: ["Content-Type": "application/json"]) | ||
} | ||
|
||
let waitExpectation = expectation(description: "Block Settings should be successfully fetched") | ||
service.fetchBlockEditorSettings(forSiteID: siteID) { (response) in | ||
service.fetchBlockEditorSettings { (response) in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd expect SwiftLint to complain about the ()
at some point. But, it should also be something it can autocorrect easily 🤷♂️
Note
This PR introduces breaking changes, and unfortunately can't be split into smaller ones (they won't be ship-able individually).
I recommend you reviewing this PR commit by commit. However, when it comes to the commit which rewrites
WordPressOrgRestApi
, I'd suggest you ignore the commit diff and review the new implementation directly.Description
Unlike the refactor to WP.com and XMLRPC API client, this PR rewrites
WordPressOrgRestApi
which deals with .org REST API.About .org REST API
Every WordPress site ships with .org REST API, which is accessible via
<site-url>/wp-json/<route>
. The same .org REST API is also available on WordPress.com, but under a different URL:https://public-api.wordpress.com/wp/v2/site/<site-id>/<route>
.Current implementation
Currently, .org REST API (represented by the
WordPressRestApi
protocol) is implemented by two classes:WordPressOrgRestApi
for self-hosted sites andWordPressComRestApi
for WP.com sites.As mentioned before, the same backend implementation is exposed by different URL path. The
WordPressRestApi
protocol has a specialrequestPath
function which the callers have to remember to use (here is an example) before sending HTTP requests. IMO, forgetting to call that special function is an easy mistake to make during development.Also, the WP.com implementation of .org REST API reuses the error handling code in
rest/vx.x
. If I understand it correctly, therest/
endpoints are entirely different to the endpoints underwp/v2
and there is no guarantee errors are reported in the same HTTP response format.New implementation
The PR rewrites
WordPressOrgRestApi
to be one single implementation which supports self-hosted sites and WordPress.com sites.The problem with different path now becomes an internal implementation detail which callers don't need to worry about (here is an example), mostly. When we need to support more .org REST API, WordPressKit crashes with an error telling developers the exact changes they need to make to add such support.
The error handling is now the same for both self-hosted sites and WP.com sites, which makes sense because they are backed by the same backend implementation.
Testing Details
I have added/updated unit tests in this PR. You can also use wordpress-mobile/WordPress-iOS#22612 to do some manual tests on the app.
CHANGELOG.md
if necessary.