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
228 lines
7.0 KiB
Swift
228 lines
7.0 KiB
Swift
class NetworkTalkPage {
|
|
let url: URL
|
|
let topics: [NetworkTopic]
|
|
var revisionId: Int?
|
|
let displayTitle: String
|
|
|
|
init(url: URL, topics: [NetworkTopic], revisionId: Int?, displayTitle: String) {
|
|
self.url = url
|
|
self.topics = topics
|
|
self.revisionId = revisionId
|
|
self.displayTitle = displayTitle
|
|
}
|
|
}
|
|
|
|
class NetworkBase: Codable {
|
|
let topics: [NetworkTopic]
|
|
}
|
|
|
|
class NetworkTopic: NSObject, Codable {
|
|
let html: String
|
|
let replies: [NetworkReply]
|
|
let sectionID: Int
|
|
let shas: NetworkTopicShas
|
|
var sort: Int?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case html
|
|
case shas
|
|
case replies
|
|
case sectionID = "id"
|
|
}
|
|
}
|
|
|
|
class NetworkTopicShas: Codable {
|
|
let html: String
|
|
let indicator: String
|
|
}
|
|
|
|
class NetworkReply: NSObject, Codable {
|
|
let html: String
|
|
let depth: Int16
|
|
let sha: String
|
|
var sort: Int!
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case html
|
|
case depth
|
|
case sha
|
|
}
|
|
}
|
|
|
|
import Foundation
|
|
import WMF
|
|
|
|
enum OldTalkPageType: Int {
|
|
case user
|
|
case article
|
|
|
|
func canonicalNamespacePrefix(for siteURL: URL) -> String? {
|
|
let namespace: PageNamespace
|
|
switch self {
|
|
case .article:
|
|
namespace = PageNamespace.talk
|
|
case .user:
|
|
namespace = PageNamespace.userTalk
|
|
}
|
|
return namespace.canonicalName + ":"
|
|
}
|
|
|
|
func titleWithCanonicalNamespacePrefix(title: String, siteURL: URL) -> String {
|
|
return (canonicalNamespacePrefix(for: siteURL) ?? "") + title
|
|
}
|
|
|
|
func titleWithoutNamespacePrefix(title: String) -> String {
|
|
if let firstColon = title.range(of: ":") {
|
|
var returnTitle = title
|
|
returnTitle.removeSubrange(title.startIndex..<firstColon.upperBound)
|
|
return returnTitle
|
|
} else {
|
|
return title
|
|
}
|
|
}
|
|
|
|
func urlTitle(for title: String) -> String? {
|
|
assert(title.contains(":"), "Title must already be prefixed with namespace.")
|
|
return title.denormalizedPageTitle
|
|
}
|
|
}
|
|
|
|
enum OldTalkPageFetcherError: Error {
|
|
case talkPageDoesNotExist
|
|
}
|
|
|
|
class OldTalkPageFetcher: Fetcher {
|
|
|
|
private let sectionUploader = WikiTextSectionUploader()
|
|
|
|
func addTopic(to title: String, siteURL: URL, subject: String, body: String, completion: @escaping (Result<[AnyHashable : Any], Error>) -> Void) {
|
|
|
|
guard let url = postURL(for: title, siteURL: siteURL) else {
|
|
completion(.failure(RequestError.invalidParameters))
|
|
return
|
|
}
|
|
|
|
sectionUploader.addSection(withSummary: subject, text: body, forArticleURL: url) { (result, error) in
|
|
if let error = error {
|
|
completion(.failure(error))
|
|
return
|
|
}
|
|
|
|
guard let result = result else {
|
|
completion(.failure(RequestError.unexpectedResponse))
|
|
return
|
|
}
|
|
|
|
completion(.success(result))
|
|
}
|
|
}
|
|
|
|
func addReply(to topic: TalkPageTopic, title: String, siteURL: URL, body: String, completion: @escaping (Result<[AnyHashable : Any], Error>) -> Void) {
|
|
|
|
guard let url = postURL(for: title, siteURL: siteURL) else {
|
|
completion(.failure(RequestError.invalidParameters))
|
|
return
|
|
}
|
|
|
|
// todo: should sectionID in CoreData be string?
|
|
sectionUploader.append(toSection: String(topic.sectionID), text: body, forArticleURL: url) { (result, error) in
|
|
if let error = error {
|
|
completion(.failure(error))
|
|
return
|
|
}
|
|
|
|
guard let result = result else {
|
|
completion(.failure(RequestError.unexpectedResponse))
|
|
return
|
|
}
|
|
|
|
completion(.success(result))
|
|
}
|
|
}
|
|
|
|
func fetchTalkPage(urlTitle: String, displayTitle: String, siteURL: URL, revisionID: Int?, completion: @escaping (Result<NetworkTalkPage, Error>) -> Void) {
|
|
|
|
guard let taskURLWithRevID = getURL(for: urlTitle, siteURL: siteURL, revisionID: revisionID),
|
|
let taskURLWithoutRevID = getURL(for: urlTitle, siteURL: siteURL, revisionID: nil) else {
|
|
completion(.failure(RequestError.invalidParameters))
|
|
return
|
|
}
|
|
|
|
// todo: track tasks/cancel
|
|
session.jsonDecodableTask(with: taskURLWithRevID) { (networkBase: NetworkBase?, response: URLResponse?, error: Error?) in
|
|
|
|
if let statusCode = (response as? HTTPURLResponse)?.statusCode,
|
|
statusCode == 404 {
|
|
completion(.failure(OldTalkPageFetcherError.talkPageDoesNotExist))
|
|
return
|
|
}
|
|
|
|
if let error = error {
|
|
completion(.failure(error))
|
|
return
|
|
}
|
|
|
|
guard let networkBase = networkBase else {
|
|
completion(.failure(RequestError.unexpectedResponse))
|
|
return
|
|
}
|
|
|
|
// update sort
|
|
// todo performance: should we go back to NSOrderedSets or move sort up into endpoint?
|
|
for (topicIndex, topic) in networkBase.topics.enumerated() {
|
|
|
|
topic.sort = topicIndex
|
|
|
|
for (replyIndex, reply) in topic.replies.enumerated() {
|
|
reply.sort = replyIndex
|
|
}
|
|
}
|
|
|
|
let talkPage = NetworkTalkPage(url: taskURLWithoutRevID, topics: networkBase.topics, revisionId: revisionID, displayTitle: displayTitle)
|
|
completion(.success(talkPage))
|
|
}
|
|
}
|
|
|
|
func getURL(for urlTitle: String, siteURL: URL) -> URL? {
|
|
return getURL(for: urlTitle, siteURL: siteURL, revisionID: nil)
|
|
}
|
|
}
|
|
|
|
// MARK: Private
|
|
|
|
private extension OldTalkPageFetcher {
|
|
|
|
func getURL(for urlTitle: String, siteURL: URL, revisionID: Int?) -> URL? {
|
|
|
|
assert(urlTitle.contains(":"), "Title must already be prefixed with namespace.")
|
|
|
|
guard siteURL.host != nil,
|
|
let percentEncodedUrlTitle = urlTitle.percentEncodedPageTitleForPathComponents else {
|
|
return nil
|
|
}
|
|
|
|
var pathComponents = ["page", "talk", percentEncodedUrlTitle]
|
|
if let revisionID = revisionID {
|
|
pathComponents.append(String(revisionID))
|
|
}
|
|
|
|
guard let taskURL = configuration.pageContentServiceAPIURLForURL(siteURL, appending: pathComponents) else {
|
|
return nil
|
|
}
|
|
|
|
return taskURL
|
|
}
|
|
|
|
func postURL(for urlTitle: String, siteURL: URL) -> URL? {
|
|
|
|
assert(urlTitle.contains(":"), "Title must already be prefixed with namespace.")
|
|
|
|
guard let host = siteURL.host else {
|
|
return nil
|
|
}
|
|
|
|
return configuration.articleURLForHost(host, languageVariantCode: siteURL.wmf_languageVariantCode, appending: [urlTitle])
|
|
}
|
|
|
|
}
|