-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Implement fetching posts/pages list in PostRepository #21814
Changes from all commits
a0c6388
4f8178a
fa641ec
ed47e2b
c980a30
4dac5b7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -228,3 +228,205 @@ final class PostRepository { | |
} | ||
|
||
} | ||
|
||
// MARK: - Posts/Pages List | ||
|
||
private final class PostRepositoryPostsSerivceRemoteOptions: NSObject, PostServiceRemoteOptions { | ||
struct Options { | ||
var statuses: [String]? | ||
var number: Int = 100 | ||
var offset: Int = 0 | ||
var order: PostServiceResultsOrder = .descending | ||
var orderBy: PostServiceResultsOrdering = .byDate | ||
var authorID: NSNumber? | ||
var search: String? | ||
var meta: String? = "autosave" | ||
} | ||
|
||
var options: Options | ||
|
||
init(options: Options) { | ||
self.options = options | ||
} | ||
|
||
func statuses() -> [String]? { | ||
options.statuses | ||
} | ||
|
||
func number() -> NSNumber { | ||
NSNumber(value: options.number) | ||
} | ||
|
||
func offset() -> NSNumber { | ||
NSNumber(value: options.offset) | ||
} | ||
|
||
func order() -> PostServiceResultsOrder { | ||
options.order | ||
} | ||
|
||
func orderBy() -> PostServiceResultsOrdering { | ||
options.orderBy | ||
} | ||
|
||
func authorID() -> NSNumber? { | ||
options.authorID | ||
} | ||
|
||
func search() -> String? { | ||
options.search | ||
} | ||
|
||
func meta() -> String? { | ||
options.meta | ||
} | ||
} | ||
|
||
private extension PostServiceRemote { | ||
|
||
func getPosts(ofType type: String, options: PostRepositoryPostsSerivceRemoteOptions) async throws -> [RemotePost] { | ||
try await withCheckedThrowingContinuation { continuation in | ||
self.getPostsOfType(type, options: self.dictionary(with: options), success: { | ||
continuation.resume(returning: $0 ?? []) | ||
}, failure: { | ||
continuation.resume(throwing: $0!) | ||
}) | ||
} | ||
} | ||
} | ||
|
||
extension PostRepository { | ||
|
||
/// Fetch posts or pages from the given site page by page. All fetched posts are saved to the local database. | ||
/// | ||
/// - Parameters: | ||
/// - type: `Post.self` and `Page.self` are the only acceptable types. | ||
/// - statuses: Filter posts or pages with given status. | ||
/// - authorUserID: Filter posts or pages that are authored by given user. | ||
/// - offset: The position of the paginated request. Pass 0 for the first page and count of already fetched results for following pages. | ||
/// - number: Number of posts or pages should be fetched. | ||
/// - blogID: The blog from which to fetch posts or pages | ||
/// - Returns: Object identifiers of the fetched posts. | ||
/// - SeeAlso: https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/posts/ | ||
func paginate<P: AbstractPost>( | ||
type: P.Type = P.self, | ||
statuses: [BasePost.Status], | ||
authorUserID: NSNumber? = nil, | ||
offset: Int, | ||
number: Int, | ||
in blogID: TaggedManagedObjectID<Blog> | ||
) async throws -> [TaggedManagedObjectID<P>] { | ||
try await fetch( | ||
type: type, | ||
statuses: statuses, | ||
authorUserID: authorUserID, | ||
range: offset..<(offset + max(number, 0)), | ||
orderBy: .byDate, | ||
descending: true, | ||
// Only delete other local posts if the current call is the first pagination request. | ||
deleteOtherLocalPosts: offset == 0, | ||
in: blogID | ||
) | ||
} | ||
|
||
/// Search posts or pages in the given site. All fetched posts are saved to the local database. | ||
/// | ||
/// - Parameters: | ||
/// - type: `Post.self` and `Page.self` are the only acceptable types. | ||
/// - input: The text input from user. Or `nil` for searching all posts or pages. | ||
/// - statuses: Filter posts or pages with given status. | ||
/// - authorUserID: Filter posts or pages that are authored by given user. | ||
/// - limit: Number of posts or pages should be fetched. | ||
/// - orderBy: The property by which to sort posts or pages. | ||
/// - descending: Whether to sort the results in descending order. | ||
/// - blogID: The blog from which to search posts or pages | ||
/// - Returns: Object identifiers of the search result. | ||
/// - SeeAlso: https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/posts/ | ||
func search<P: AbstractPost>( | ||
type: P.Type = P.self, | ||
input: String?, | ||
statuses: [BasePost.Status], | ||
authorUserID: NSNumber? = nil, | ||
limit: Int, | ||
orderBy: PostServiceResultsOrdering, | ||
descending: Bool, | ||
in blogID: TaggedManagedObjectID<Blog> | ||
) async throws -> [TaggedManagedObjectID<P>] { | ||
try await fetch( | ||
type: type, | ||
searchInput: input, | ||
statuses: statuses, | ||
authorUserID: authorUserID, | ||
range: 0..<max(limit, 0), | ||
orderBy: orderBy, | ||
descending: descending, | ||
deleteOtherLocalPosts: false, | ||
in: blogID | ||
) | ||
} | ||
|
||
private func fetch<P: AbstractPost>( | ||
type: P.Type, | ||
searchInput: String? = nil, | ||
statuses: [BasePost.Status]?, | ||
authorUserID: NSNumber?, | ||
range: Range<Int>, | ||
orderBy: PostServiceResultsOrdering = .byDate, | ||
descending: Bool = true, | ||
deleteOtherLocalPosts: Bool, | ||
in blogID: TaggedManagedObjectID<Blog> | ||
) async throws -> [TaggedManagedObjectID<P>] { | ||
assert(type == Post.self || type == Page.self, "Only support fetching Post or Page") | ||
assert(range.lowerBound >= 0) | ||
|
||
let postType: String | ||
if type == Post.self { | ||
postType = "post" | ||
} else if type == Page.self { | ||
postType = "page" | ||
} else { | ||
// There is an assertion above to ensure the app doesn't fall into this case. | ||
return [] | ||
Comment on lines
+388
to
+389
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have you considered Or, is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just don't want to duplicate the assertion code and its messaging. 😄 |
||
} | ||
|
||
let remote = try await coreDataStack.performQuery { [remoteFactory] context in | ||
let blog = try context.existingObject(with: blogID) | ||
return remoteFactory.forBlog(blog) | ||
} | ||
guard let remote else { | ||
throw PostRepository.Error.remoteAPIUnavailable | ||
} | ||
|
||
let options = PostRepositoryPostsSerivceRemoteOptions(options: .init( | ||
statuses: statuses?.strings, | ||
number: range.count, | ||
offset: range.lowerBound, | ||
order: descending ? .descending : .ascending, | ||
orderBy: orderBy, | ||
authorID: authorUserID, | ||
search: searchInput | ||
)) | ||
let remotePosts = try await remote.getPosts(ofType: postType, options: options) | ||
|
||
let updatedPosts = try await coreDataStack.performAndSave { context in | ||
let updatedPosts = PostHelper.merge( | ||
remotePosts, | ||
ofType: postType, | ||
withStatuses: statuses?.strings, | ||
byAuthor: authorUserID, | ||
for: try context.existingObject(with: blogID), | ||
purgeExisting: deleteOtherLocalPosts, | ||
in: context | ||
) | ||
return updatedPosts.map { | ||
guard let post = $0 as? P else { | ||
fatalError("Expecting a \(postType) as \(type), but got \($0)") | ||
} | ||
return TaggedManagedObjectID(post) | ||
} | ||
} | ||
|
||
return updatedPosts | ||
} | ||
|
||
} |
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 making this and the other computed vars?
I don't know if there's a recommendation somewhere. It might just be my personal preference, but I find that this kind of getters read better as properties than functions. I guess methods give me the idea that something is happening under the hood, whereas properties feel more like reading information.
Update: Never mind... This conforms to the
PostServiceRemoteOptions
protocol (a protocol that should be astruct
by now...) and therefore they need to befunc
: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.
Yeah, Objective-C...