Packages/MoonriseKit/Sources/Feeds/News.swift
46 lines · 2005 bytes
1import Foundation
2
3/// Space news from the Spaceflight News API (The Space Devs): headline, summary and a link to the article.
4public struct NewsArticle: Sendable, Identifiable, Hashable, Decodable {
5 public var id: Int
6 public var title: String
7 public var summary: String
8 public var url: URL
9 public var image: URL?
10 public var site: String
11 public var published: Date
12
13 public static let url = URL(string: "https://api.spaceflightnewsapi.net/v4/articles/?limit=40")!
14
15 public static func latest(session: URLSession = .shared) async throws -> [NewsArticle] {
16 try decode(await session.data(from: url).0)
17 }
18
19 /// Decodes a page, leaving out Astronomy Picture of the Day posts, whose summaries are page navigation.
20 static func decode(_ data: Data) throws -> [NewsArticle] {
21 let d = JSONDecoder()
22 d.dateDecodingStrategy = .iso8601
23 return try d.decode(Page.self, from: data).results.filter { !$0.title.hasPrefix("APOD:") }
24 }
25
26 private struct Page: Decodable { var results: [NewsArticle] }
27
28 private enum CodingKeys: String, CodingKey {
29 case id, title, summary, url
30 case image = "image_url", site = "news_site", published = "published_at"
31 }
32
33 public init(from decoder: Decoder) throws {
34 let c = try decoder.container(keyedBy: CodingKeys.self)
35 id = try c.decode(Int.self, forKey: .id)
36 title = try c.decode(String.self, forKey: .title)
37 summary = try c.decode(String.self, forKey: .summary).trimmingCharacters(in: .whitespacesAndNewlines)
38 url = try c.decode(URL.self, forKey: .url)
39 site = try c.decode(String.self, forKey: .site)
40 published = try c.decode(Date.self, forKey: .published)
41 // Some sites give http image URLs, which App Transport Security blocks.
42 image = (try? c.decodeIfPresent(String.self, forKey: .image)).flatMap { s in
43 URL(string: s.hasPrefix("http://") ? "https://" + s.dropFirst(7) : s)
44 }
45 }
46}