Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ let package = Package(
],
resources: [
.process("Resources/json/feed.json"),
.process("Resources/json/feed_v1_1.json"),
.process("Resources/xml/Ampersand.xml"),
.process("Resources/xml/Atom + XHTML.xml"),
.process("Resources/xml/Atom.xml"),
Expand Down
1 change: 1 addition & 0 deletions Package@swift-5.9.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ let package = Package(
],
resources: [
.process("Resources/json/feed.json"),
.process("Resources/json/feed_v1_1.json"),
.process("Resources/xml/Ampersand.xml"),
.process("Resources/xml/Atom + XHTML.xml"),
.process("Resources/xml/Atom.xml"),
Expand Down
40 changes: 38 additions & 2 deletions Sources/FeedKit/Feeds/JSON/JSONFeed.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ public struct JSONFeed {
icon: String? = nil,
favicon: String? = nil,
author: JSONFeedAuthor? = nil,
authors: [JSONFeedAuthor]? = nil,
language: String? = nil,
expired: Bool? = nil,
hubs: [JSONFeedHub]? = nil,
items: [JSONFeedItem]? = nil
Expand All @@ -52,9 +54,18 @@ public struct JSONFeed {
self.icon = icon
self.favicon = favicon
self.author = author
self.authors = authors
self.language = language
self.expired = expired
self.hubs = hubs
self.items = items
// The `authors` and `language` members were introduced in JSON Feed 1.1,
// so a feed that uses them is a 1.1 feed.
if authors != nil || language != nil ||
items?.contains(where: { $0.authors != nil || $0.language != nil }) == true
{
version = "https://jsonfeed.org/version/1.1"
}
}

// MARK: Public
Expand Down Expand Up @@ -109,9 +120,23 @@ public struct JSONFeed {

/// (optional, object) specifies the feed author. The author object has
/// several members. These are all optional - but if you provide an author
/// object, then at least one is required.
/// object, then at least one is required. Deprecated in JSON Feed 1.1 in
/// favour of `authors`.
public var author: JSONFeedAuthor?

/// (optional, array of objects) specifies one or more feed authors. The
/// author object has several members. These are all optional - but if you
/// provide an author object, then at least one is required. Added in JSON
/// Feed 1.1, replacing the singular `author`. Feed readers should always
/// prefer `authors` if present.
public var authors: [JSONFeedAuthor]?

/// (optional, string) is the primary language for the feed in the format
/// specified in RFC 5646. The value is usually a 2-letter language tag from
/// ISO 639-1, optionally followed by a region tag. (Examples: en or en-US.)
/// Added in JSON Feed 1.1.
public var language: String?

/// (optional, boolean) says whether or not the feed is finished - that is,
/// whether or not it will ever update again. A feed for a temporary event,
/// such as an instance of the Olympics, could expire. If the value is true,
Expand Down Expand Up @@ -162,6 +187,8 @@ extension JSONFeed: Codable {
case favicon
case expired
case author
case authors
case language
case hubs
case items
}
Expand All @@ -179,6 +206,8 @@ extension JSONFeed: Codable {
try container.encodeIfPresent(favicon, forKey: .favicon)
try container.encodeIfPresent(expired, forKey: .expired)
try container.encodeIfPresent(author, forKey: .author)
try container.encodeIfPresent(authors, forKey: .authors)
try container.encodeIfPresent(language, forKey: .language)
try container.encodeIfPresent(hubs, forKey: .hubs)
try container.encodeIfPresent(items, forKey: .items)
}
Expand All @@ -195,7 +224,14 @@ extension JSONFeed: Codable {
icon = try values.decodeIfPresent(String.self, forKey: .icon)
favicon = try values.decodeIfPresent(String.self, forKey: .favicon)
expired = try values.decodeIfPresent(Bool.self, forKey: .expired)
author = try values.decodeIfPresent(JSONFeedAuthor.self, forKey: .author)
let author = try values.decodeIfPresent(JSONFeedAuthor.self, forKey: .author)
let authors = try values.decodeIfPresent([JSONFeedAuthor].self, forKey: .authors)
// JSON Feed 1.1 deprecated the singular `author` in favour of `authors`,
// and 1.1 feeds commonly omit `author` entirely. Fall back to the first
// entry of `authors` so `author` remains usable either way.
self.authors = authors
self.author = author ?? authors?.first
language = try values.decodeIfPresent(String.self, forKey: .language)
hubs = try values.decodeIfPresent([JSONFeedHub].self, forKey: .hubs)
items = try values.decodeIfPresent([JSONFeedItem].self, forKey: .items)
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/FeedKit/Feeds/JSON/JSONFeedAuthor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ extension JSONFeedAuthor: Codable {

public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decode(String.self, forKey: .name)
name = try values.decodeIfPresent(String.self, forKey: .name)
url = try values.decodeIfPresent(String.self, forKey: .url)
avatar = try values.decodeIfPresent(String.self, forKey: .avatar)
}
Expand Down
31 changes: 29 additions & 2 deletions Sources/FeedKit/Feeds/JSON/JSONFeedItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ public struct JSONFeedItem {
datePublished: Date? = nil,
dateModified: Date? = nil,
author: JSONFeedAuthor? = nil,
authors: [JSONFeedAuthor]? = nil,
language: String? = nil,
tags: [String]? = nil,
attachments: [JSONFeedAttachment]? = nil
) {
Expand All @@ -56,6 +58,8 @@ public struct JSONFeedItem {
self.datePublished = datePublished
self.dateModified = dateModified
self.author = author
self.authors = authors
self.language = language
self.tags = tags
self.attachments = attachments
}
Expand Down Expand Up @@ -127,9 +131,21 @@ public struct JSONFeedItem {

/// (optional, object) has the same structure as the top-level author.
/// If not specified in an item, then the top-level author, if present, is the
/// author of the item.
/// author of the item. Deprecated in JSON Feed 1.1 in favour of `authors`.
public var author: JSONFeedAuthor?

/// (optional, array of objects) has the same structure as the top-level
/// authors. If not specified in an item, then the top-level authors, if
/// present, are the authors of the item. Added in JSON Feed 1.1, replacing
/// the singular `author`. Feed readers should always prefer `authors` if
/// present.
public var authors: [JSONFeedAuthor]?

/// (optional, string) is the language for this item, in the format specified
/// in RFC 5646. It's only necessary when the item's language is different
/// from the feed's language. Added in JSON Feed 1.1.
public var language: String?

/// (optional, array of strings) can have any plain text values you want. Tags
/// tend to be just one word, but they may be anything. Note: they are not the
/// equivalent of Twitter hashtags. Some blogging systems and other feed
Expand Down Expand Up @@ -169,6 +185,8 @@ extension JSONFeedItem: Codable {
case date_modified
case tags
case author
case authors
case language
case attachments
}

Expand All @@ -187,6 +205,8 @@ extension JSONFeedItem: Codable {
try container.encodeIfPresent(dateModified, forKey: .date_modified)
try container.encodeIfPresent(tags, forKey: .tags)
try container.encodeIfPresent(author, forKey: .author)
try container.encodeIfPresent(authors, forKey: .authors)
try container.encodeIfPresent(language, forKey: .language)
try container.encodeIfPresent(attachments, forKey: .attachments)
}

Expand All @@ -209,7 +229,14 @@ extension JSONFeedItem: Codable {
datePublished = try values.decodeIfPresent(Date.self, forKey: .date_published)
dateModified = try values.decodeIfPresent(Date.self, forKey: .date_modified)
tags = try values.decodeIfPresent([String].self, forKey: .tags)
author = try values.decodeIfPresent(JSONFeedAuthor.self, forKey: .author)
let author = try values.decodeIfPresent(JSONFeedAuthor.self, forKey: .author)
let authors = try values.decodeIfPresent([JSONFeedAuthor].self, forKey: .authors)
// JSON Feed 1.1 deprecated the singular `author` in favour of `authors`,
// and 1.1 feeds commonly omit `author` entirely. Fall back to the first
// entry of `authors` so `author` remains usable either way.
self.authors = authors
self.author = author ?? authors?.first
language = try values.decodeIfPresent(String.self, forKey: .language)
attachments = try values.decodeIfPresent([JSONFeedAttachment].self, forKey: .attachments)
}
}
35 changes: 35 additions & 0 deletions Tests/FeedKitTests/Resources/json/feed_v1_1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"version": "https://jsonfeed.org/version/1.1",
"title": "Title",
"home_page_url": "https://example.org/",
"feed_url": "https://example.org/feed.json",
"language": "en-US",
"authors": [
{
"name": "Brent Simmons",
"url": "http://example.org/",
"avatar": "https://example.org/avatar.png"
},
{
"name": "John Gruber"
}
],
"items": [
{
"id": "1",
"url": "https://example.org/initial-post",
"content_html": "<p>Hello, world!</p>",
"language": "pt-PT",
"authors": [
{
"url": "http://example.org/no-name"
}
]
},
{
"id": "2",
"url": "https://example.org/second-post",
"content_text": "Hello again, world!"
}
]
}
41 changes: 41 additions & 0 deletions Tests/FeedKitTests/Tests/JSONTests + Mocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,51 @@ extension JSONTests {
datePublished: nil,
dateModified: nil,
author: nil,
authors: nil,
language: nil,
tags: nil,
attachments: nil
)
]
)
}
}

extension JSONTests {
var mockV1_1: JSONFeed {
.init(
title: "Title",
homePageURL: "https://example.org/",
feedURL: "https://example.org/feed.json",
author: .init(
name: "Brent Simmons",
url: "http://example.org/",
avatar: "https://example.org/avatar.png"
),
authors: [
.init(
name: "Brent Simmons",
url: "http://example.org/",
avatar: "https://example.org/avatar.png"
),
.init(name: "John Gruber")
],
language: "en-US",
items: [
.init(
id: "1",
url: "https://example.org/initial-post",
contentHtml: "<p>Hello, world!</p>",
author: .init(url: "http://example.org/no-name"),
authors: [.init(url: "http://example.org/no-name")],
language: "pt-PT"
),
.init(
id: "2",
url: "https://example.org/second-post",
contentText: "Hello again, world!"
)
]
)
}
}
44 changes: 44 additions & 0 deletions Tests/FeedKitTests/Tests/JSONTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,48 @@ struct JSONTests: FeedKitTestable {
// Then
#expect(expected == actual)
}

@Test
func jsonFeedV1_1() throws {
// Given
let data = data(resource: "feed_v1_1", withExtension: "json")
let expected: JSONFeed = mockV1_1

// When
let actual = try JSONFeed(data: data)

// Then
#expect(expected == actual)
}

@Test
func jsonStringV1_1() throws {
// Given
let expected: JSONFeed = mockV1_1

// When
let jsonString = try expected.toJSONString(formatted: true)
let actual = try JSONFeed(string: jsonString)

// Then
#expect(expected == actual)
}

/// JSON Feed 1.1 deprecated the singular `author`, so 1.1 feeds in the wild
/// often only carry `authors`. See https://github.com/nmdias/FeedKit/issues/223
@Test
func jsonFeedAuthorFallsBackToAuthors() throws {
// Given
let data = data(resource: "feed_v1_1", withExtension: "json")

// When
let feed = try JSONFeed(data: data)

// Then
#expect(feed.author?.name == "Brent Simmons")
#expect(feed.authors?.count == 2)
#expect(feed.language == "en-US")
#expect(feed.items?.first?.author?.url == "http://example.org/no-name")
#expect(feed.items?.first?.language == "pt-PT")
}
}
Loading