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
609 lines
24 KiB
Swift
609 lines
24 KiB
Swift
import Foundation
|
|
enum SignificantEventsDecodeError: Error {
|
|
case unableToParseIntoTypedEvents
|
|
}
|
|
|
|
public struct SignificantEvents: Decodable {
|
|
public let nextRvStartId: UInt?
|
|
public let sha: String?
|
|
private let untypedEvents: [UntypedEvent]
|
|
public let typedEvents: [TypedEvent]
|
|
public let summary: Summary
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case nextRvStartId
|
|
case sha
|
|
case untypedEvents = "timeline"
|
|
case typedEvents
|
|
case summary
|
|
}
|
|
|
|
public struct Summary: Decodable {
|
|
public let earliestTimestampString: String
|
|
public let numChanges: UInt
|
|
public let numUsers: UInt
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case earliestTimestampString = "earliestTimestamp"
|
|
case numChanges
|
|
case numUsers
|
|
}
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
nextRvStartId = try? container.decode(UInt.self, forKey: .nextRvStartId)
|
|
sha = try? container.decode(String.self, forKey: .sha)
|
|
summary = try container.decode(Summary.self, forKey: .summary)
|
|
let untypedEvents = try container.decode([UntypedEvent].self, forKey: .untypedEvents)
|
|
|
|
var typedEvents: [TypedEvent] = []
|
|
|
|
for untypedEvent in untypedEvents {
|
|
switch untypedEvent.outputType {
|
|
case .small:
|
|
if let event = Event.Small(untypedEvent: untypedEvent) {
|
|
typedEvents.append(.small(event))
|
|
}
|
|
case .large:
|
|
if let event = Event.Large(untypedEvent: untypedEvent) {
|
|
typedEvents.append(.large(event))
|
|
}
|
|
case .vandalismRevert:
|
|
if let event = Event.VandalismRevert(untypedEvent: untypedEvent) {
|
|
typedEvents.append(.vandalismRevert(event))
|
|
}
|
|
case .newTalkPageTopic:
|
|
if let event = Event.NewTalkPageTopic(untypedEvent: untypedEvent) {
|
|
typedEvents.append(.newTalkPageTopic(event))
|
|
}
|
|
}
|
|
}
|
|
|
|
// zero untyped events is a valid case if the user has paged to the end of the endpoint cache
|
|
// unTypedEvents > 0 and typedEvents == 0 is invalid, meaning all events failed to convert
|
|
guard typedEvents.count > 0 || untypedEvents.count == 0 else {
|
|
throw SignificantEventsDecodeError.unableToParseIntoTypedEvents
|
|
}
|
|
|
|
self.typedEvents = typedEvents
|
|
self.untypedEvents = untypedEvents
|
|
}
|
|
|
|
public enum SnippetType: Int, Decodable {
|
|
case addedLine = 1
|
|
case addedAndDeletedInLine = 3
|
|
case addedAndDeletedInMovedLine = 5
|
|
}
|
|
|
|
public enum EventOutputType: String, Decodable {
|
|
case large = "large-change"
|
|
case small = "small-change"
|
|
case newTalkPageTopic = "new-talk-page-topic"
|
|
case vandalismRevert = "vandalism-revert"
|
|
}
|
|
|
|
public enum ChangeOutputType: String, Decodable {
|
|
case addedText = "added-text"
|
|
case deletedText = "deleted-text"
|
|
case newTemplate = "new-template"
|
|
}
|
|
|
|
public enum TypedEvent {
|
|
case large(Event.Large)
|
|
case small(Event.Small)
|
|
case vandalismRevert(Event.VandalismRevert)
|
|
case newTalkPageTopic(Event.NewTalkPageTopic)
|
|
}
|
|
|
|
public enum TypedChange {
|
|
case addedText(Change.AddedText)
|
|
case deletedText(Change.DeletedText)
|
|
case newTemplate(Change.NewTemplates)
|
|
}
|
|
}
|
|
|
|
// MARK: Events
|
|
|
|
public extension SignificantEvents {
|
|
|
|
struct Event {
|
|
|
|
public struct Large {
|
|
let outputType: EventOutputType
|
|
public let revId: UInt
|
|
public let parentId: UInt
|
|
public let timestampString: String
|
|
public let user: String
|
|
public let userId: UInt
|
|
public let userGroups: [String]?
|
|
public let userEditCount: UInt?
|
|
public let typedChanges: [TypedChange]
|
|
|
|
init?(untypedEvent: UntypedEvent) {
|
|
guard let revId = untypedEvent.revId,
|
|
let parentId = untypedEvent.parentId,
|
|
let timestampString = untypedEvent.timestampString,
|
|
let user = untypedEvent.user,
|
|
let userId = untypedEvent.userId,
|
|
let untypedChanges = untypedEvent.untypedChanges else {
|
|
return nil
|
|
}
|
|
|
|
self.outputType = untypedEvent.outputType
|
|
self.revId = revId
|
|
self.parentId = parentId
|
|
self.timestampString = timestampString
|
|
self.user = user
|
|
self.userId = userId
|
|
self.userGroups = untypedEvent.userGroups
|
|
self.userEditCount = untypedEvent.userEditCount
|
|
|
|
var changes: [TypedChange] = []
|
|
|
|
for untypedChange in untypedChanges {
|
|
switch untypedChange.outputType {
|
|
case .addedText:
|
|
if let change = Change.AddedText(untypedChange: untypedChange) {
|
|
changes.append(.addedText(change))
|
|
}
|
|
case .deletedText:
|
|
if let change = Change.DeletedText(untypedChange: untypedChange) {
|
|
changes.append(.deletedText(change))
|
|
}
|
|
case .newTemplate:
|
|
if let change = Change.NewTemplates(untypedChange: untypedChange) {
|
|
changes.append(.newTemplate(change))
|
|
}
|
|
}
|
|
}
|
|
|
|
guard changes.count == untypedChanges.count else {
|
|
return nil
|
|
}
|
|
|
|
self.typedChanges = changes
|
|
}
|
|
}
|
|
|
|
public struct Small: Equatable {
|
|
let outputType: EventOutputType
|
|
public let revId: UInt
|
|
public let parentId: UInt
|
|
public let timestampString: String
|
|
|
|
fileprivate init?(untypedEvent: UntypedEvent) {
|
|
guard let revId = untypedEvent.revId,
|
|
let parentId = untypedEvent.parentId,
|
|
let timestampString = untypedEvent.timestampString else {
|
|
return nil
|
|
}
|
|
|
|
self.outputType = untypedEvent.outputType
|
|
self.revId = revId
|
|
self.parentId = parentId
|
|
self.timestampString = timestampString
|
|
}
|
|
|
|
public static func == (lhs: SignificantEvents.Event.Small, rhs: SignificantEvents.Event.Small) -> Bool {
|
|
return lhs.revId == rhs.revId
|
|
}
|
|
}
|
|
|
|
public struct VandalismRevert {
|
|
let outputType: EventOutputType
|
|
public let revId: UInt
|
|
public let parentId: UInt
|
|
public let timestampString: String
|
|
public let user: String
|
|
public let userId: UInt
|
|
public let sections: [String]
|
|
public let userGroups: [String]?
|
|
public let userEditCount: UInt?
|
|
|
|
fileprivate init?(untypedEvent: UntypedEvent) {
|
|
guard let revId = untypedEvent.revId,
|
|
let parentId = untypedEvent.parentId,
|
|
let timestampString = untypedEvent.timestampString,
|
|
let user = untypedEvent.user,
|
|
let userId = untypedEvent.userId,
|
|
let sections = untypedEvent.sections else {
|
|
return nil
|
|
}
|
|
|
|
self.outputType = untypedEvent.outputType
|
|
self.revId = revId
|
|
self.parentId = parentId
|
|
self.timestampString = timestampString
|
|
self.user = user
|
|
self.userId = userId
|
|
self.sections = sections
|
|
self.userGroups = untypedEvent.userGroups
|
|
self.userEditCount = untypedEvent.userEditCount
|
|
}
|
|
}
|
|
|
|
public struct NewTalkPageTopic {
|
|
let outputType: EventOutputType
|
|
let revId: UInt
|
|
let parentId: UInt
|
|
public let timestampString: String
|
|
public let user: String
|
|
public let userId: UInt
|
|
public let section: String?
|
|
public let snippet: String
|
|
public let userGroups: [String]?
|
|
public let userEditCount: UInt?
|
|
|
|
fileprivate init?(untypedEvent: UntypedEvent) {
|
|
guard let revId = untypedEvent.revId,
|
|
let parentId = untypedEvent.parentId,
|
|
let timestampString = untypedEvent.timestampString,
|
|
let user = untypedEvent.user,
|
|
let userId = untypedEvent.userId,
|
|
let snippet = untypedEvent.snippet else {
|
|
return nil
|
|
}
|
|
|
|
self.outputType = untypedEvent.outputType
|
|
self.revId = revId
|
|
self.parentId = parentId
|
|
self.timestampString = timestampString
|
|
self.user = user
|
|
self.userId = userId
|
|
self.section = untypedEvent.section
|
|
self.snippet = snippet
|
|
self.userGroups = untypedEvent.userGroups
|
|
self.userEditCount = untypedEvent.userEditCount
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Changes
|
|
|
|
public extension SignificantEvents {
|
|
|
|
struct Change {
|
|
|
|
public struct AddedText {
|
|
let outputType: ChangeOutputType
|
|
public let sections: [String]
|
|
public let snippet: String?
|
|
public let snippetType: SnippetType
|
|
public let characterCount: UInt
|
|
|
|
fileprivate init?(untypedChange: UntypedChange) {
|
|
guard let snippetType = untypedChange.snippetType,
|
|
let characterCount = untypedChange.characterCount else {
|
|
return nil
|
|
}
|
|
|
|
self.outputType = untypedChange.outputType
|
|
self.sections = untypedChange.sections
|
|
self.snippet = untypedChange.snippet
|
|
self.snippetType = snippetType
|
|
self.characterCount = characterCount
|
|
}
|
|
}
|
|
|
|
public struct DeletedText {
|
|
let outputType: ChangeOutputType
|
|
public let sections: [String]
|
|
public let characterCount: UInt
|
|
|
|
fileprivate init?(untypedChange: UntypedChange) {
|
|
guard let characterCount = untypedChange.characterCount else {
|
|
return nil
|
|
}
|
|
|
|
self.outputType = untypedChange.outputType
|
|
self.sections = untypedChange.sections
|
|
self.characterCount = characterCount
|
|
}
|
|
}
|
|
|
|
public struct NewTemplates {
|
|
let outputType: ChangeOutputType
|
|
public let sections: [String]
|
|
private let untypedTemplates: [[String: String]]
|
|
public let typedTemplates: [Template]
|
|
|
|
fileprivate init?(untypedChange: UntypedChange) {
|
|
guard let untypedTemplates = untypedChange.untypedTemplates else {
|
|
return nil
|
|
}
|
|
|
|
var typedTemplates: [Template] = []
|
|
self.outputType = untypedChange.outputType
|
|
self.sections = untypedChange.sections
|
|
self.untypedTemplates = untypedTemplates
|
|
|
|
for untypedTemplate in untypedTemplates {
|
|
guard let name = untypedTemplate["name"] else {
|
|
continue
|
|
}
|
|
if name.localizedCaseInsensitiveContains("cite") {
|
|
if name.localizedCaseInsensitiveContains("book"), let bookCitation = Citation.Book(dict: untypedTemplate) {
|
|
typedTemplates.append(.bookCitation(bookCitation))
|
|
} else if name.localizedCaseInsensitiveContains("journal"), let journalCitation = Citation.Journal(dict: untypedTemplate) {
|
|
typedTemplates.append(.journalCitation(journalCitation))
|
|
} else if name.localizedCaseInsensitiveContains("web"), let webCitation = Citation.Website(dict: untypedTemplate) {
|
|
typedTemplates.append(.websiteCitation(webCitation))
|
|
} else if name.localizedCaseInsensitiveContains("news"), let newsCitation = Citation.News(dict: untypedTemplate) {
|
|
typedTemplates.append(.newsCitation(newsCitation))
|
|
}
|
|
} else if name.localizedCaseInsensitiveContains("short description"), let articleDescription = ArticleDescription(dict: untypedTemplate) {
|
|
typedTemplates.append(.articleDescription(articleDescription))
|
|
}
|
|
}
|
|
|
|
self.typedTemplates = typedTemplates
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Templates
|
|
|
|
public extension SignificantEvents {
|
|
|
|
enum Template {
|
|
case bookCitation(Citation.Book)
|
|
case articleDescription(ArticleDescription)
|
|
case journalCitation(Citation.Journal)
|
|
case newsCitation(Citation.News)
|
|
case websiteCitation(Citation.Website)
|
|
}
|
|
|
|
struct Citation {
|
|
|
|
// https://en.wikipedia.org/wiki/Template:Cite_book/TemplateData
|
|
public struct Book {
|
|
public let title: String
|
|
public let lastName: String?
|
|
public let firstName: String?
|
|
public let yearPublished: String?
|
|
public let locationPublished: String?
|
|
public let publisher: String?
|
|
public let pagesCited: String?
|
|
public let isbn: String?
|
|
|
|
init?(dict: [String: String]) {
|
|
guard let title = dict.nonEmptyValueForKey(key: "title") else {
|
|
return nil
|
|
}
|
|
|
|
self.title = title
|
|
|
|
let batch1 = dict.nonEmptyValueForKey(key: "last") ??
|
|
dict.nonEmptyValueForKey(key: "last1") ??
|
|
dict.nonEmptyValueForKey(key: "author") ??
|
|
dict.nonEmptyValueForKey(key: "author1") ??
|
|
dict.nonEmptyValueForKey(key: "author1-last")
|
|
let batch2 = dict.nonEmptyValueForKey(key: "author-last") ??
|
|
dict.nonEmptyValueForKey(key: "surname1") ??
|
|
dict.nonEmptyValueForKey(key: "author-last1") ??
|
|
dict.nonEmptyValueForKey(key: "subject1") ??
|
|
dict.nonEmptyValueForKey(key: "surname")
|
|
let batch3 = dict.nonEmptyValueForKey(key: "author-last") ??
|
|
dict.nonEmptyValueForKey(key: "subject")
|
|
|
|
self.lastName = batch1 ?? batch2 ?? batch3
|
|
|
|
self.firstName = dict.nonEmptyValueForKey(key: "first") ??
|
|
dict.nonEmptyValueForKey(key: "given") ??
|
|
dict.nonEmptyValueForKey(key: "author-first") ??
|
|
dict.nonEmptyValueForKey(key: "first1") ??
|
|
dict.nonEmptyValueForKey(key: "given1") ??
|
|
dict.nonEmptyValueForKey(key: "author-first1") ??
|
|
dict.nonEmptyValueForKey(key: "author1-first")
|
|
|
|
self.yearPublished = dict.nonEmptyValueForKey(key: "year")
|
|
self.locationPublished = dict.nonEmptyValueForKey(key: "location") ??
|
|
dict.nonEmptyValueForKey(key: "place")
|
|
|
|
self.publisher = dict.nonEmptyValueForKey(key: "publisher") ??
|
|
dict.nonEmptyValueForKey(key: "distributor") ??
|
|
dict.nonEmptyValueForKey(key: "institution") ??
|
|
dict.nonEmptyValueForKey(key: "newsgroup")
|
|
|
|
self.pagesCited = dict.nonEmptyValueForKey(key: "pages") ??
|
|
dict.nonEmptyValueForKey(key: "pp")
|
|
|
|
self.isbn = dict.nonEmptyValueForKey(key: "isbn", caseInsensitive: true) ??
|
|
dict.nonEmptyValueForKey(key: "isbn13", caseInsensitive: true)
|
|
}
|
|
}
|
|
|
|
// https://en.wikipedia.org/wiki/Template:Cite_journal#TemplateData
|
|
public struct Journal {
|
|
public let lastName: String?
|
|
public let firstName: String?
|
|
public let sourceDateString: String?
|
|
public let title: String
|
|
public let journal: String
|
|
public let urlString: String?
|
|
public let volumeNumber: String?
|
|
public let pages: String?
|
|
public let database: String?
|
|
|
|
init?(dict: [String: String]) {
|
|
guard let title = dict.nonEmptyValueForKey(key: "title"),
|
|
let journal = dict.nonEmptyValueForKey(key: "journal") else {
|
|
return nil
|
|
}
|
|
|
|
self.title = title
|
|
self.journal = journal
|
|
|
|
self.lastName = dict.nonEmptyValueForKey(key: "last") ??
|
|
dict.nonEmptyValueForKey(key: "author") ??
|
|
dict.nonEmptyValueForKey(key: "author1") ??
|
|
dict.nonEmptyValueForKey(key: "authors") ??
|
|
dict.nonEmptyValueForKey(key: "last1")
|
|
|
|
self.firstName = dict.nonEmptyValueForKey(key: "first") ??
|
|
dict.nonEmptyValueForKey(key: "first1")
|
|
|
|
self.sourceDateString = dict.nonEmptyValueForKey(key: "date")
|
|
self.urlString = dict.nonEmptyValueForKey(key: "url", caseInsensitive: true)
|
|
self.volumeNumber = dict.nonEmptyValueForKey(key: "volume")
|
|
self.pages = dict.nonEmptyValueForKey(key: "pages")
|
|
self.database = dict.nonEmptyValueForKey(key: "via")
|
|
}
|
|
}
|
|
|
|
// https://en.wikipedia.org/wiki/Template:Cite_news#TemplateData
|
|
public struct News {
|
|
public let lastName: String?
|
|
public let firstName: String?
|
|
public let sourceDateString: String?
|
|
public let title: String
|
|
public let urlString: String?
|
|
public let publication: String?
|
|
public let accessDateString: String?
|
|
|
|
init?(dict: [String: String]) {
|
|
guard let title = dict.nonEmptyValueForKey(key: "title") else {
|
|
return nil
|
|
}
|
|
|
|
self.title = title
|
|
self.lastName = dict.nonEmptyValueForKey(key: "last") ??
|
|
dict.nonEmptyValueForKey(key: "last1") ??
|
|
dict.nonEmptyValueForKey(key: "author") ??
|
|
dict.nonEmptyValueForKey(key: "author1") ??
|
|
dict.nonEmptyValueForKey(key: "authors")
|
|
|
|
self.firstName = dict.nonEmptyValueForKey(key: "first") ??
|
|
dict.nonEmptyValueForKey(key: "first1")
|
|
|
|
self.sourceDateString = dict.nonEmptyValueForKey(key: "date")
|
|
self.publication = dict.nonEmptyValueForKey(key: "work") ??
|
|
dict.nonEmptyValueForKey(key: "journal") ??
|
|
dict.nonEmptyValueForKey(key: "magazine") ??
|
|
dict.nonEmptyValueForKey(key: "periodical") ??
|
|
dict.nonEmptyValueForKey(key: "newspaper") ??
|
|
dict.nonEmptyValueForKey(key: "website")
|
|
|
|
self.urlString = dict.nonEmptyValueForKey(key: "url", caseInsensitive: true)
|
|
self.accessDateString = dict.nonEmptyValueForKey(key: "access-date") ?? dict.nonEmptyValueForKey(key: "accessdate")
|
|
}
|
|
}
|
|
|
|
// https://en.wikipedia.org/wiki/Template:Cite_web#TemplateData
|
|
public struct Website {
|
|
|
|
public let urlString: String
|
|
public let title: String
|
|
public let publisher: String?
|
|
public let accessDateString: String?
|
|
public let archiveDateString: String?
|
|
public let archiveDotOrgUrlString: String?
|
|
|
|
init?(dict: [String: String]) {
|
|
guard let title = dict.nonEmptyValueForKey(key: "title"),
|
|
let urlString = dict.nonEmptyValueForKey(key: "url", caseInsensitive: true) else {
|
|
return nil
|
|
}
|
|
|
|
self.title = title
|
|
self.urlString = urlString
|
|
|
|
self.publisher = dict.nonEmptyValueForKey(key: "publisher") ??
|
|
dict.nonEmptyValueForKey(key: "website") ??
|
|
dict.nonEmptyValueForKey(key: "work")
|
|
|
|
self.accessDateString = dict.nonEmptyValueForKey(key: "access-date") ?? dict.nonEmptyValueForKey(key: "accessdate")
|
|
self.archiveDateString = dict.nonEmptyValueForKey(key: "archive-date") ?? dict.nonEmptyValueForKey(key: "archivedate")
|
|
self.archiveDotOrgUrlString = dict.nonEmptyValueForKey(key: "archive-url") ?? dict.nonEmptyValueForKey(key: "archiveurl")
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ArticleDescription {
|
|
public let text: String
|
|
|
|
init?(dict: [String: String]) {
|
|
guard let text = dict.nonEmptyValueForKey(key: "1") else {
|
|
return nil
|
|
}
|
|
|
|
self.text = text
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
|
|
// MARK: Untyped
|
|
|
|
public extension SignificantEvents {
|
|
struct UntypedEvent: Decodable {
|
|
let outputType: EventOutputType
|
|
let revId: UInt?
|
|
let parentId: UInt?
|
|
let timestampString: String?
|
|
let user: String?
|
|
let userId: UInt?
|
|
let userGroups: [String]?
|
|
let userEditCount: UInt?
|
|
let count: UInt?
|
|
let sections: [String]?
|
|
let section: String?
|
|
let snippet: String?
|
|
let untypedChanges: [UntypedChange]?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case revId = "revid"
|
|
case parentId = "parentid"
|
|
case timestampString = "timestamp"
|
|
case outputType
|
|
case user
|
|
case userId = "userid"
|
|
case userGroups
|
|
case userEditCount
|
|
case count
|
|
case sections
|
|
case section
|
|
case snippet
|
|
case untypedChanges = "significantChanges"
|
|
}
|
|
}
|
|
|
|
struct UntypedChange: Decodable {
|
|
let outputType: ChangeOutputType
|
|
let sections: [String]
|
|
let snippet: String?
|
|
let snippetType: SnippetType?
|
|
let characterCount: UInt?
|
|
let untypedTemplates: [[String: String]]?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case outputType
|
|
case sections
|
|
case snippet
|
|
case snippetType
|
|
case characterCount
|
|
case untypedTemplates = "templates"
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension Dictionary where Key == String, Value == String {
|
|
func nonEmptyValueForKey(key: String, caseInsensitive: Bool = false) -> String? {
|
|
guard let key = caseInsensitive
|
|
? keys.first(where: {$0.caseInsensitiveCompare(key) == .orderedSame})
|
|
: key else {
|
|
return nil
|
|
}
|
|
|
|
if let value = self[key], !value.isEmpty {
|
|
return value
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|