deep-linking-sample/Apps/Wikipedia/WMF Framework/ArticleCacheResourceDBWriting.swift
Javier Cicchelli 9bcdaa697b [Setup] Basic project structure (#1)
This PR contains all the work related to setting up this project as required to implement the [Assignment](https://repo.rock-n-code.com/rock-n-code/deep-linking-assignment/wiki/Assignment) on top, as intended.

To summarise this work:
- [x] created a new **Xcode** project;
- [x] cloned the `Wikipedia` app and inserted it into the **Xcode** project;
- [x] created the `Locations` app and also, its `Libraries` package;
- [x] created the `Shared` package to share dependencies between the apps;
- [x] added a `Makefile` file and implemented some **environment** and **help** commands.

Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Reviewed-on: rock-n-code/deep-linking-assignment#1
2023-04-08 18:37:13 +00:00

207 lines
8.0 KiB
Swift

import Foundation
struct ImageAndResourceURLs {
let offlineResourcesURLs: [URL]
let mediaListURLs: [URL]
let imageInfoURLs: [URL]
}
enum ImageAndResourceResult {
case success(ImageAndResourceURLs)
case failure(Error)
}
typealias ImageAndResourceCompletion = (ImageAndResourceResult) -> Void
protocol ArticleCacheResourceDBWriting: CacheDBWriting {
func fetchMediaListURLs(request: URLRequest, groupKey: String, completion: @escaping (Result<[ArticleFetcher.MediaListItem], ArticleCacheDBWriterError>) -> Void)
func fetchOfflineResourceURLs(request: URLRequest, groupKey: String, completion: @escaping (Result<[URL], ArticleCacheDBWriterError>) -> Void)
func cacheURLs(groupKey: String, mustHaveURLRequests: [URLRequest], niceToHaveURLRequests: [URLRequest], completion: @escaping ((SaveResult) -> Void))
var articleFetcher: ArticleFetcher { get }
var imageInfoFetcher: MWKImageInfoFetcher { get }
var context: NSManagedObjectContext { get }
}
extension ArticleCacheResourceDBWriting {
func fetchMediaListURLs(request: URLRequest, groupKey: String, completion: @escaping (Result<[ArticleFetcher.MediaListItem], ArticleCacheDBWriterError>) -> Void) {
guard request.url != nil else {
completion(.failure(.missingListURLInRequest))
return
}
let untrackKey = UUID().uuidString
let task = articleFetcher.fetchMediaListURLs(with: request) { [weak self] (result) in
defer {
self?.untrackTask(untrackKey: untrackKey, from: groupKey)
}
switch result {
case .success(let items):
completion(.success(items))
case .failure(let error):
completion(.failure(.failureFetchingMediaList(error)))
}
}
if let task = task {
trackTask(untrackKey: untrackKey, task: task, to: groupKey)
}
}
func fetchOfflineResourceURLs(request: URLRequest, groupKey: String, completion: @escaping (Result<[URL], ArticleCacheDBWriterError>) -> Void) {
guard request.url != nil else {
completion(.failure(.missingListURLInRequest))
return
}
let untrackKey = UUID().uuidString
let task = articleFetcher.fetchOfflineResourceURLs(with: request) { [weak self] (result) in
defer {
self?.untrackTask(untrackKey: untrackKey, from: groupKey)
}
switch result {
case .success(let urls):
completion(.success(urls))
case .failure(let error):
completion(.failure(.failureFetchingOfflineResourceList(error)))
}
}
if let task = task {
trackTask(untrackKey: untrackKey, task: task, to: groupKey)
}
}
func cacheURLs(groupKey: String, mustHaveURLRequests: [URLRequest], niceToHaveURLRequests: [URLRequest], completion: @escaping ((SaveResult) -> Void)) {
context.perform {
guard let group = CacheDBWriterHelper.fetchOrCreateCacheGroup(with: groupKey, in: self.context) else {
completion(.failure(ArticleCacheDBWriterError.failureFetchOrCreateCacheGroup))
return
}
for urlRequest in mustHaveURLRequests {
guard let url = urlRequest.url,
let itemKey = self.fetcher.itemKeyForURLRequest(urlRequest) else {
completion(.failure(ArticleCacheDBWriterError.unableToDetermineItemKey))
return
}
// note, we purposefully do not set variant here. We need to wait until CacheFileWriter determines if the response varies on language, then set it when we call markDownloaded
guard let item = CacheDBWriterHelper.fetchOrCreateCacheItem(with: url, itemKey: itemKey, variant: nil, in: self.context) else {
completion(.failure(ArticleCacheDBWriterError.failureFetchOrCreateMustHaveCacheItem))
return
}
group.addToCacheItems(item)
group.addToMustHaveCacheItems(item)
}
for urlRequest in niceToHaveURLRequests {
guard let url = urlRequest.url,
let itemKey = self.fetcher.itemKeyForURLRequest(urlRequest) else {
continue
}
guard let item = CacheDBWriterHelper.fetchOrCreateCacheItem(with: url, itemKey: itemKey, variant: nil, in: self.context) else {
continue
}
group.addToCacheItems(item)
}
CacheDBWriterHelper.save(moc: self.context, completion: completion)
}
}
func fetchImageAndResourceURLsForArticleURL(_ articleURL: URL, groupKey: CacheController.GroupKey, completion: @escaping ImageAndResourceCompletion) {
var mobileHTMLOfflineResourcesRequest: URLRequest
var mobileHTMLMediaListRequest: URLRequest
do {
mobileHTMLOfflineResourcesRequest = try articleFetcher.mobileHTMLOfflineResourcesRequest(articleURL: articleURL)
mobileHTMLMediaListRequest = try articleFetcher.mobileHTMLMediaListRequest(articleURL: articleURL)
} catch let error {
completion(.failure(error))
return
}
var mobileHtmlOfflineResourceURLs: [URL] = []
var mediaListURLs: [URL] = []
var imageInfoURLs: [URL] = []
var mediaListError: Error?
var mobileHtmlOfflineResourceError: Error?
let group = DispatchGroup()
group.enter()
fetchOfflineResourceURLs(request: mobileHTMLOfflineResourcesRequest, groupKey: groupKey) { (result) in
defer {
group.leave()
}
switch result {
case .success(let urls):
mobileHtmlOfflineResourceURLs = urls
case .failure(let error):
mobileHtmlOfflineResourceError = error
}
}
group.enter()
fetchMediaListURLs(request: mobileHTMLMediaListRequest, groupKey: groupKey) { (result) in
defer {
group.leave()
}
switch result {
case .success(let items):
mediaListURLs = items.map { $0.imageURL }
let imageTitles = items.map { $0.imageTitle }
let dedupedTitles = Set(imageTitles)
// add imageInfoFetcher's urls for deduped titles (for captions/licensing info in gallery)
for title in dedupedTitles {
if let imageInfoURL = self.imageInfoFetcher.galleryInfoURL(forImageTitles: [title], fromSiteURL: articleURL) {
imageInfoURLs.append(imageInfoURL)
}
}
case .failure(let error):
mediaListError = error
}
}
group.notify(queue: DispatchQueue.global(qos: .default)) {
if let mediaListError = mediaListError {
let result = ImageAndResourceResult.failure(mediaListError)
completion(result)
return
}
if let mobileHtmlOfflineResourceError = mobileHtmlOfflineResourceError {
let result = ImageAndResourceResult.failure(mobileHtmlOfflineResourceError)
completion(result)
return
}
let result = ImageAndResourceURLs(offlineResourcesURLs: mobileHtmlOfflineResourceURLs, mediaListURLs: mediaListURLs, imageInfoURLs: imageInfoURLs)
completion(.success(result))
}
}
}