2017-06-25 20:48:44 +02:00
|
|
|
|
//
|
|
|
|
|
// JSONFeedParser.swift
|
|
|
|
|
// RSParser
|
|
|
|
|
//
|
|
|
|
|
// Created by Brent Simmons on 6/25/17.
|
|
|
|
|
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
|
|
|
|
|
|
// See https://jsonfeed.org/version/1
|
|
|
|
|
|
|
|
|
|
public struct JSONFeedParser {
|
|
|
|
|
|
2017-06-25 23:06:01 +02:00
|
|
|
|
public static func parse(_ parserData: ParserData) throws -> ParsedFeed? {
|
2017-06-25 20:48:44 +02:00
|
|
|
|
|
|
|
|
|
do {
|
2017-06-25 23:06:01 +02:00
|
|
|
|
guard let parsedObject = try JSONSerialization.jsonObject(with: parserData.data) as? JSONDictionary else {
|
|
|
|
|
throw FeedParserError(.invalidJSON)
|
|
|
|
|
}
|
2017-06-25 20:48:44 +02:00
|
|
|
|
|
|
|
|
|
guard let version = parsedObject["version"] as? String, version.hasPrefix("https://jsonfeed.org/version/") else {
|
|
|
|
|
throw FeedParserError(.jsonFeedVersionNotFound)
|
|
|
|
|
}
|
|
|
|
|
guard let itemsArray = parsedObject["items"] as? JSONArray else {
|
|
|
|
|
throw FeedParserError(.jsonFeedItemsNotFound)
|
|
|
|
|
}
|
|
|
|
|
guard let title = parsedObject["title"] as? String else {
|
|
|
|
|
throw FeedParserError(.jsonFeedTitleNotFound)
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-25 23:06:01 +02:00
|
|
|
|
let authors = parseAuthors(parsedObject)
|
2017-06-25 20:48:44 +02:00
|
|
|
|
let homePageURL = parsedObject["home_page_url"] as? String
|
2017-06-25 23:06:01 +02:00
|
|
|
|
let feedURL = parsedObject["feed_url"] as? String ?? parserData.url
|
2017-06-25 20:48:44 +02:00
|
|
|
|
let feedDescription = parsedObject["description"] as? String
|
|
|
|
|
let nextURL = parsedObject["next_url"] as? String
|
|
|
|
|
let iconURL = parsedObject["icon_url"] as? String
|
|
|
|
|
let faviconURL = parsedObject["favicon_url"] as? String
|
|
|
|
|
let expired = parsedObject["expired"] as? Bool ?? false
|
|
|
|
|
let hubs = parseHubs(parsedObject)
|
|
|
|
|
|
2017-07-02 02:22:19 +02:00
|
|
|
|
let items = parseItems(itemsArray, parserData.url)
|
2017-06-25 20:48:44 +02:00
|
|
|
|
|
|
|
|
|
return ParsedFeed(type: .jsonFeed, title: title, homePageURL: homePageURL, feedURL: feedURL, feedDescription: feedDescription, nextURL: nextURL, iconURL: iconURL, faviconURL: faviconURL, authors: authors, expired: expired, hubs: hubs, items: items)
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
catch { throw error }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private extension JSONFeedParser {
|
|
|
|
|
|
2017-09-10 20:02:05 +02:00
|
|
|
|
static func parseAuthors(_ dictionary: JSONDictionary) -> Set<ParsedAuthor>? {
|
2017-06-25 20:48:44 +02:00
|
|
|
|
|
|
|
|
|
guard let authorDictionary = dictionary["author"] as? JSONDictionary else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-25 23:06:01 +02:00
|
|
|
|
let name = authorDictionary["name"] as? String
|
|
|
|
|
let url = authorDictionary["url"] as? String
|
|
|
|
|
let avatar = authorDictionary["avatar"] as? String
|
2017-06-25 20:48:44 +02:00
|
|
|
|
if name == nil && url == nil && avatar == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
let parsedAuthor = ParsedAuthor(name: name, url: url, avatarURL: avatar, emailAddress: nil)
|
2017-09-10 20:02:05 +02:00
|
|
|
|
return Set([parsedAuthor])
|
2017-06-25 20:48:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-09-10 19:53:24 +02:00
|
|
|
|
static func parseHubs(_ dictionary: JSONDictionary) -> Set<ParsedHub>? {
|
2017-06-25 20:48:44 +02:00
|
|
|
|
|
|
|
|
|
guard let hubsArray = dictionary["hubs"] as? JSONArray else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-10 19:53:24 +02:00
|
|
|
|
let hubs = hubsArray.flatMap { (hubDictionary) -> ParsedHub? in
|
|
|
|
|
guard let hubURL = hubDictionary["url"] as? String, let hubType = hubDictionary["type"] as? String else {
|
2017-06-25 20:48:44 +02:00
|
|
|
|
return nil
|
|
|
|
|
}
|
2017-09-10 19:53:24 +02:00
|
|
|
|
return ParsedHub(type: hubType, url: hubURL)
|
2017-06-25 20:48:44 +02:00
|
|
|
|
}
|
2017-09-10 19:53:24 +02:00
|
|
|
|
return hubs.isEmpty ? nil : Set(hubs)
|
2017-06-25 20:48:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-09-10 03:46:58 +02:00
|
|
|
|
static func parseItems(_ itemsArray: JSONArray, _ feedURL: String) -> Set<ParsedItem> {
|
2017-06-25 20:48:44 +02:00
|
|
|
|
|
2017-09-10 03:46:58 +02:00
|
|
|
|
return Set(itemsArray.flatMap { (oneItemDictionary) -> ParsedItem? in
|
2017-07-02 02:22:19 +02:00
|
|
|
|
return parseItem(oneItemDictionary, feedURL)
|
2017-09-10 03:46:58 +02:00
|
|
|
|
})
|
2017-06-25 20:48:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-07-02 02:22:19 +02:00
|
|
|
|
static func parseItem(_ itemDictionary: JSONDictionary, _ feedURL: String) -> ParsedItem? {
|
2017-06-25 20:48:44 +02:00
|
|
|
|
|
|
|
|
|
guard let uniqueID = parseUniqueID(itemDictionary) else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let contentHTML = itemDictionary["content_html"] as? String
|
|
|
|
|
let contentText = itemDictionary["content_text"] as? String
|
|
|
|
|
if contentHTML == nil && contentText == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2017-11-18 21:41:15 +01:00
|
|
|
|
let decodedContentHTML = contentHTML?.rsparser_stringByDecodingHTMLEntities()
|
2017-06-25 20:48:44 +02:00
|
|
|
|
|
|
|
|
|
let url = itemDictionary["url"] as? String
|
|
|
|
|
let externalURL = itemDictionary["external_url"] as? String
|
|
|
|
|
let title = itemDictionary["title"] as? String
|
|
|
|
|
let summary = itemDictionary["summary"] as? String
|
|
|
|
|
let imageURL = itemDictionary["image"] as? String
|
|
|
|
|
let bannerImageURL = itemDictionary["banner_image"] as? String
|
|
|
|
|
|
2017-06-25 23:06:01 +02:00
|
|
|
|
let datePublished = parseDate(itemDictionary["date_published"] as? String)
|
|
|
|
|
let dateModified = parseDate(itemDictionary["date_modified"] as? String)
|
2017-06-25 20:48:44 +02:00
|
|
|
|
|
|
|
|
|
let authors = parseAuthors(itemDictionary)
|
2017-09-10 20:18:15 +02:00
|
|
|
|
var tags: Set<String>? = nil
|
|
|
|
|
if let tagsArray = itemDictionary["tags"] as? [String] {
|
|
|
|
|
tags = Set(tagsArray)
|
|
|
|
|
}
|
2017-06-25 20:48:44 +02:00
|
|
|
|
let attachments = parseAttachments(itemDictionary)
|
|
|
|
|
|
2017-11-18 21:41:15 +01:00
|
|
|
|
return ParsedItem(syncServiceID: nil, uniqueID: uniqueID, feedURL: feedURL, url: url, externalURL: externalURL, title: title, contentHTML: decodedContentHTML, contentText: contentText, summary: summary, imageURL: imageURL, bannerImageURL: bannerImageURL, datePublished: datePublished, dateModified: dateModified, authors: authors, tags: tags, attachments: attachments)
|
2017-06-25 20:48:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-06-25 23:06:01 +02:00
|
|
|
|
static func parseUniqueID(_ itemDictionary: JSONDictionary) -> String? {
|
2017-06-25 20:48:44 +02:00
|
|
|
|
|
|
|
|
|
if let uniqueID = itemDictionary["id"] as? String {
|
|
|
|
|
return uniqueID // Spec says it must be a string
|
|
|
|
|
}
|
|
|
|
|
// Spec also says that if it’s a number, even though that’s incorrect, it should be coerced to a string.
|
|
|
|
|
if let uniqueID = itemDictionary["id"] as? Int {
|
|
|
|
|
return "\(uniqueID)"
|
|
|
|
|
}
|
|
|
|
|
if let uniqueID = itemDictionary["id"] as? Double {
|
|
|
|
|
return "\(uniqueID)"
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-25 23:06:01 +02:00
|
|
|
|
static func parseDate(_ dateString: String?) -> Date? {
|
2017-06-25 20:48:44 +02:00
|
|
|
|
|
|
|
|
|
guard let dateString = dateString, !dateString.isEmpty else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return RSDateWithString(dateString)
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-10 20:18:15 +02:00
|
|
|
|
static func parseAttachments(_ itemDictionary: JSONDictionary) -> Set<ParsedAttachment>? {
|
2017-06-25 20:48:44 +02:00
|
|
|
|
|
|
|
|
|
guard let attachmentsArray = itemDictionary["attachments"] as? JSONArray else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2017-09-10 20:18:15 +02:00
|
|
|
|
return Set(attachmentsArray.flatMap { (oneAttachmentObject) -> ParsedAttachment? in
|
2017-06-25 20:48:44 +02:00
|
|
|
|
return parseAttachment(oneAttachmentObject)
|
2017-09-10 20:18:15 +02:00
|
|
|
|
})
|
2017-06-25 20:48:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-06-25 23:06:01 +02:00
|
|
|
|
static func parseAttachment(_ attachmentObject: JSONDictionary) -> ParsedAttachment? {
|
2017-06-25 20:48:44 +02:00
|
|
|
|
|
|
|
|
|
guard let url = attachmentObject["url"] as? String else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
guard let mimeType = attachmentObject["mime_type"] as? String else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let title = attachmentObject["title"] as? String
|
|
|
|
|
let sizeInBytes = attachmentObject["size_in_bytes"] as? Int
|
|
|
|
|
let durationInSeconds = attachmentObject["duration_in_seconds"] as? Int
|
2017-06-25 23:06:01 +02:00
|
|
|
|
|
2017-06-25 20:48:44 +02:00
|
|
|
|
return ParsedAttachment(url: url, mimeType: mimeType, title: title, sizeInBytes: sizeInBytes, durationInSeconds: durationInSeconds)
|
|
|
|
|
}
|
|
|
|
|
}
|