diff --git a/NOICommunity.xcodeproj/project.pbxproj b/NOICommunity.xcodeproj/project.pbxproj index 82cad72..a0a0cb9 100644 --- a/NOICommunity.xcodeproj/project.pbxproj +++ b/NOICommunity.xcodeproj/project.pbxproj @@ -158,7 +158,6 @@ 319C0E9A26F3483600C6D38B /* UIControl+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319C0E9926F3483600C6D38B /* UIControl+Combine.swift */; }; 319C0EA026F4790300C6D38B /* CalendarAdditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319C0E9F26F4790300C6D38B /* CalendarAdditions.swift */; }; 319C4653282BB32400946AC7 /* ArticlesClient in Frameworks */ = {isa = PBXBuildFile; productRef = 319C4652282BB32400946AC7 /* ArticlesClient */; }; - 319C4655282BB32400946AC7 /* ArticlesClientLive in Frameworks */ = {isa = PBXBuildFile; productRef = 319C4654282BB32400946AC7 /* ArticlesClientLive */; }; 319C465A282BBD8000946AC7 /* NewsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319C4657282BB80F00946AC7 /* NewsListViewModel.swift */; }; 319C465C282BBE5100946AC7 /* XCTestCase+awaitPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319C465B282BBE5100946AC7 /* XCTestCase+awaitPublisher.swift */; }; 319C465E282BCC5D00946AC7 /* collectNext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319C465D282BCC5D00946AC7 /* collectNext.swift */; }; @@ -477,7 +476,6 @@ 31E058F22812F18800D1F7FE /* KeychainAccess in Frameworks */, 319C4653282BB32400946AC7 /* ArticlesClient in Frameworks */, 3182F4A427DB3841005ADDAF /* AppPreferencesClientLive in Frameworks */, - 319C4655282BB32400946AC7 /* ArticlesClientLive in Frameworks */, 317B6F9C28118BD6008D07C0 /* AuthClient in Frameworks */, 3182F4AC27DB3841005ADDAF /* EventShortTypesClientLive in Frameworks */, 311E0EC62825157800404DCE /* FirebaseMessaging in Frameworks */, @@ -1093,7 +1091,6 @@ 31E058F12812F18800D1F7FE /* KeychainAccess */, 311E0EC52825157800404DCE /* FirebaseMessaging */, 319C4652282BB32400946AC7 /* ArticlesClient */, - 319C4654282BB32400946AC7 /* ArticlesClientLive */, 317EC888283BB83E00F30B95 /* PeopleClient */, 317EC88A283BB83E00F30B95 /* PeopleClientLive */, 3181B9202B1E12BD000D2A0F /* Core */, @@ -2362,10 +2359,6 @@ isa = XCSwiftPackageProductDependency; productName = ArticlesClient; }; - 319C4654282BB32400946AC7 /* ArticlesClientLive */ = { - isa = XCSwiftPackageProductDependency; - productName = ArticlesClientLive; - }; 31B1926E2C6A2136009872E9 /* FirebaseCrashlytics */ = { isa = XCSwiftPackageProductDependency; package = 311E0EC42825157800404DCE /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; diff --git a/NOICommunity/NewsFeature/View Models/NewsDetailsViewModel.swift b/NOICommunity/NewsFeature/View Models/NewsDetailsViewModel.swift index 527762a..724c820 100644 --- a/NOICommunity/NewsFeature/View Models/NewsDetailsViewModel.swift +++ b/NOICommunity/NewsFeature/View Models/NewsDetailsViewModel.swift @@ -32,8 +32,6 @@ final class NewsDetailsViewModel { lazy var showAskAQuestionPublisher = showAskAQuestionSubject .eraseToAnyPublisher() - private var fetchRequestCancellable: AnyCancellable? - init( articlesClient: ArticlesClient, availableNews: Article?, @@ -45,24 +43,9 @@ final class NewsDetailsViewModel { } func refreshNewsDetails(newsId: String) { - isLoading = true - - fetchRequestCancellable = articlesClient.detail(newsId) - .receive(on: DispatchQueue.main) - .sink( - receiveCompletion: { [weak self] completion in - self?.isLoading = false - - switch completion { - case .finished: - break - case .failure(let error): - self?.error = error - } - }, - receiveValue: { [weak self] in - self?.result = $0 - }) + Task(priority: .userInitiated) { [weak self] in + await self?.performRefreshNewsDetails(newsId: newsId) + } } func showExternalLink(sender: Any?) { @@ -74,3 +57,22 @@ final class NewsDetailsViewModel { } } + +// MARK: Private APIs + +private extension NewsDetailsViewModel { + + func performRefreshNewsDetails(newsId: String) async { + isLoading = true + defer { + isLoading = false + } + + do { + result = try await articlesClient.getArticle(newsId: newsId) + } catch { + self.error = error + } + } + +} diff --git a/NOICommunity/NewsFeature/View Models/NewsListViewModel.swift b/NOICommunity/NewsFeature/View Models/NewsListViewModel.swift index f63b395..ac7c4a1 100644 --- a/NOICommunity/NewsFeature/View Models/NewsListViewModel.swift +++ b/NOICommunity/NewsFeature/View Models/NewsListViewModel.swift @@ -53,66 +53,9 @@ final class NewsListViewModel { } func fetchNews(refresh: Bool = false) { - guard nextPage != nil || refresh - else { return } - - let pageNumber: Int - - if refresh { - pageNumber = firstPage - } else { - pageNumber = nextPage! - } - - let currentNewsIds: [String] - if refresh { - currentNewsIds = [] - } else { - currentNewsIds = newsIds - } - - isLoadingFirstPage = pageNumber == firstPage - isLoading = true - - var articlesListPublisher = articlesClient.list( - Date(), - "noi-communityapp", - pageSize, - pageNumber - ) - - if refresh { - articlesListPublisher = articlesListPublisher - .delay(for: 0.3, scheduler: RunLoop.main) - .eraseToAnyPublisher() - } - - fetchRequestCancellable = articlesListPublisher - .receive(on: DispatchQueue.main) - .sink( - receiveCompletion: { [weak self] completion in - self?.isLoadingFirstPage = false - self?.isLoading = false - - switch completion { - case .finished: - break - case .failure(let error): - self?.error = error - } - }, - receiveValue: { [weak self] pagination in - guard let self = self - else { return } - - self.nextPage = pagination.nextPage - - guard let newItems = pagination.items - else { return } - - newItems.forEach { self.idToNews[$0.id] = $0 } - self.newsIds = currentNewsIds + newItems.map(\.id) - }) + Task(priority: .userInitiated) { [weak self] in + await self?.performFetchNews(refresh: refresh) + } } func news(withId newsId: String) -> Article { @@ -131,7 +74,53 @@ final class NewsListViewModel { // MARK: Private APIs private extension NewsListViewModel { - + + func performFetchNews(refresh: Bool = false) async { + guard nextPage != nil || refresh + else { return } + + let pageNumber: Int + + if refresh { + pageNumber = firstPage + } else { + pageNumber = nextPage! + } + + let currentNewsIds: [String] + if refresh { + currentNewsIds = [] + } else { + currentNewsIds = newsIds + } + + isLoadingFirstPage = pageNumber == firstPage + isLoading = true + defer { + isLoadingFirstPage = false + isLoading = false + } + + do { + let pagination = try await articlesClient.getArticleList( + startDate: Date(), + publishedOn: "noi-communityapp", + pageSize: pageSize, + pageNumber: pageNumber + ) + + nextPage = pagination.nextPage + + if let newItems = pagination.items { + newItems.forEach { idToNews[$0.id] = $0 } + newsIds = currentNewsIds + newItems.map(\.id) + } + } catch { + self.error = error + } + + } + func configureBindings() { refreshCancellable = NotificationCenter .default diff --git a/NOICommunity/SceneDelegate.swift b/NOICommunity/SceneDelegate.swift index 2a29d9d..aa3b8d5 100644 --- a/NOICommunity/SceneDelegate.swift +++ b/NOICommunity/SceneDelegate.swift @@ -18,7 +18,7 @@ import EventShortTypesClientLive import Core import AuthClientLive import AuthStateStorageClient -import ArticlesClientLive +import ArticlesClient import PeopleClientLive #if DEBUG @@ -77,7 +77,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { return .live() } }(), - articleClient: .live(), + articleClient: ArticlesClientImplementation( + baseURL: EventsFeatureConstants.clientBaseURL, + transport: URLSession.shared + ), peopleClient: .live(baseURL: MeetConstant.clientBaseURL) ) }() diff --git a/NOICommunityLib/Package.swift b/NOICommunityLib/Package.swift index a11bc96..990a744 100644 --- a/NOICommunityLib/Package.swift +++ b/NOICommunityLib/Package.swift @@ -56,10 +56,6 @@ let package = Package( name: "ArticlesClient", targets: ["ArticlesClient"] ), - .library( - name: "ArticlesClientLive", - targets: ["ArticlesClientLive"] - ), .library( name: "PeopleClient", targets: ["PeopleClient"] @@ -154,14 +150,9 @@ let package = Package( ), .target( name: "ArticlesClient", - dependencies: [] - ), - .target( - name: "ArticlesClientLive", dependencies: [ - "Core", - "ArticlesClient", - ] + "Core" + ] ), .target( name: "PeopleClient", diff --git a/NOICommunityLib/Sources/ArticlesClientLive/Endpoints.swift b/NOICommunityLib/Sources/ArticlesClient/Endpoints+ArticleClient.swift similarity index 90% rename from NOICommunityLib/Sources/ArticlesClientLive/Endpoints.swift rename to NOICommunityLib/Sources/ArticlesClient/Endpoints+ArticleClient.swift index 018848a..0e4bed5 100644 --- a/NOICommunityLib/Sources/ArticlesClientLive/Endpoints.swift +++ b/NOICommunityLib/Sources/ArticlesClient/Endpoints+ArticleClient.swift @@ -3,15 +3,14 @@ // SPDX-License-Identifier: AGPL-3.0-or-later // -// Endpoints.swift -// ArticlesClientLive +// Endpoints+ArticleClient.swift +// ArticlesClient // // Created by Matteo Matassoni on 11/05/22. // import Foundation import Core -import ArticlesClient private let dateFormatter: DateFormatter = { dateFormatter in dateFormatter.calendar = Calendar(identifier: .iso8601) @@ -24,16 +23,18 @@ private let dateFormatter: DateFormatter = { dateFormatter in extension Endpoint { static func articleList( - startDate: Date, + startDate: Date?, publishedon: String?, pageSize: Int?, pageNumber: Int? ) -> Endpoint { Self(path: "/v1/Article") { - URLQueryItem( - name: "startDate", - value: dateFormatter.string(from: startDate) - ) + if let startDate { + URLQueryItem( + name: "startDate", + value: dateFormatter.string(from: startDate) + ) + } if let publishedon = publishedon { URLQueryItem( diff --git a/NOICommunityLib/Sources/ArticlesClient/Implementations/ArticlesClientImplementation.swift b/NOICommunityLib/Sources/ArticlesClient/Implementations/ArticlesClientImplementation.swift new file mode 100644 index 0000000..87bf29c --- /dev/null +++ b/NOICommunityLib/Sources/ArticlesClient/Implementations/ArticlesClientImplementation.swift @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: NOI Techpark +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// +// ArticlesClientImplementation.swift +// NOICommunityLib +// +// Created by Matteo Matassoni on 05/12/24. +// + +import Foundation +import Core + +public final class ArticlesClientImplementation: ArticlesClient { + + private let baseURL: URL + + private let transport: Transport + + private let jsonDecoder: JSONDecoder = { + let jsonDecoder = JSONDecoder() + + jsonDecoder.dateDecodingStrategy = .custom { decoder in + let container = try decoder.singleValueContainer() + let dateStr = try container.decode(String.self) + + let dateFormatter = DateFormatter() + dateFormatter.calendar = Calendar(identifier: .iso8601) + dateFormatter.timeZone = TimeZone(identifier: "Europe/Rome") + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZZZ" + if let date = dateFormatter.date(from: dateStr) { + return date + } + + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZZZ" + if let date = dateFormatter.date(from: dateStr) { + return date + } + + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" + if let date = dateFormatter.date(from: dateStr) { + return date + } + + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" + if let date = dateFormatter.date(from: dateStr) { + return date + } + + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Cannot decode date string \(dateStr)" + ) + } + + jsonDecoder.keyDecodingStrategy = .convertFromPascalCase + return jsonDecoder + }() + + public init( + baseURL: URL, + transport: Transport + ) { + self.baseURL = baseURL + self.transport = transport + .checkingStatusCodes() + .addingJSONHeaders() + } + + public func getArticleList( + startDate: Date?, + publishedOn: String?, + pageSize: Int?, + pageNumber: Int? + ) async throws -> ArticleListResponse { + let request = Endpoint + .articleList( + startDate: startDate, + publishedon: publishedOn, + pageSize: pageSize, + pageNumber: pageNumber + ) + .makeRequest(withBaseURL: baseURL) + + let (data, _) = try await transport.send(request: request) + + try Task.checkCancellation() + + let myArticleListResponse = try jsonDecoder.decode( + MyArticleListResponse.self, + from: data + ) + return .init(from: myArticleListResponse) + } + + public func getArticle(newsId: String) async throws -> Article { + let request = Endpoint + .article(id: newsId) + .makeRequest(withBaseURL: baseURL) + + let (data, _) = try await transport.send(request: request) + + try Task.checkCancellation() + + return try jsonDecoder.decode(Article.self, from: data) + } + +} + diff --git a/NOICommunityLib/Sources/ArticlesClient/Interface.swift b/NOICommunityLib/Sources/ArticlesClient/Interface.swift deleted file mode 100644 index a6ee8fc..0000000 --- a/NOICommunityLib/Sources/ArticlesClient/Interface.swift +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-FileCopyrightText: NOI Techpark -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -// -// Interface.swift -// ArticlesClient -// -// Created by Matteo Matassoni on 10/05/22. -// - -import Foundation -import Combine - -public struct ArticlesClient { - - public var list: (Date, String?, Int?, Int?) -> AnyPublisher - - public typealias ArticleId = String - public var detail: (String) -> AnyPublisher - - public init( - list: @escaping (Date, String?, Int?, Int?) -> AnyPublisher, - detail: @escaping (String) -> AnyPublisher - ) { - self.list = list - self.detail = detail - } -} diff --git a/NOICommunityLib/Sources/ArticlesClient/Interfaces/ArticlesClient.swift b/NOICommunityLib/Sources/ArticlesClient/Interfaces/ArticlesClient.swift new file mode 100644 index 0000000..97487f4 --- /dev/null +++ b/NOICommunityLib/Sources/ArticlesClient/Interfaces/ArticlesClient.swift @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: NOI Techpark +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// +// ArticlesClient.swift +// ArticlesClient +// +// Created by Matteo Matassoni on 10/05/22. +// + +import Foundation + +public protocol ArticlesClient { + + func getArticleList( + startDate: Date?, + publishedOn: String?, + pageSize: Int?, + pageNumber: Int? + ) async throws -> ArticleListResponse + + func getArticle(newsId: String) async throws -> Article + +} + + + + diff --git a/NOICommunityLib/Sources/ArticlesClient/Mocks.swift b/NOICommunityLib/Sources/ArticlesClient/Mocks.swift deleted file mode 100644 index 6ad35f8..0000000 --- a/NOICommunityLib/Sources/ArticlesClient/Mocks.swift +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-FileCopyrightText: NOI Techpark -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -//// -//// Mocks.swift -//// ArticlesClient -//// -//// Created by Matteo Matassoni on 10/05/22. -//// -// -//import Foundation -//import Combine -// -//extension ArticlesClient { -// -// public static let empty = Self( -// list: { _, _, _ in -// Just(ArticleListResponse( -// totalResults: 0, -// totalPages: 0, -// currentPage: 1, -// previousPage: nil, -// nextPage: nil, -// items: [] -// )) -// .setFailureType(to: Error.self) -// .eraseToAnyPublisher() -// }, -// detail: { _, _ in -// Fail(error: NSError(domain: "", code: 1)) -// .eraseToAnyPublisher() -// } -// ) -// -// public static let happyPath = empty -// -// public static let failed = Self( -// list: { _, _, _ in -// Fail(error: NSError(domain: "", code: 1)) -// .eraseToAnyPublisher() -// }, -// detail: { _, _ in -// Fail(error: NSError(domain: "", code: 1)) -// .eraseToAnyPublisher() -// } -// ) -//} diff --git a/NOICommunityLib/Sources/ArticlesClient/Models.swift b/NOICommunityLib/Sources/ArticlesClient/Models.swift index 06a5d66..5822dda 100644 --- a/NOICommunityLib/Sources/ArticlesClient/Models.swift +++ b/NOICommunityLib/Sources/ArticlesClient/Models.swift @@ -166,3 +166,57 @@ extension Article { } } + +// MARK: - ArticleListResponse + +struct MyArticleListResponse: Codable, Equatable { + + let totalResults: Int + + let totalPages: Int + + let currentPage: Int + + let previousPageURL: URL? + + let nextPageURL: URL? + + let items: [Article]? + + private enum CodingKeys: String, CodingKey { + case totalResults + case totalPages + case currentPage + case previousPageURL = "previousPage" + case nextPageURL = "nextPage" + case items + } + +} + +private func extractPageNumber(url: URL) -> Int? { + let urlComponents = URLComponents( + url: url, + resolvingAgainstBaseURL: true + ) + return urlComponents? + .queryItems? + .first { $0.name == "pagenumber"}? + .value + .flatMap(Int.init) +} + +extension ArticleListResponse { + + init(from response: MyArticleListResponse) { + self.init( + totalResults: response.totalResults, + totalPages: response.totalPages, + currentPage: response.currentPage, + previousPage: response.previousPageURL.flatMap(extractPageNumber(url:)), + nextPage: response.nextPageURL.flatMap(extractPageNumber(url:)), + items: response.items + ) + } + +} diff --git a/NOICommunityLib/Sources/ArticlesClientLive/Live.swift b/NOICommunityLib/Sources/ArticlesClientLive/Live.swift deleted file mode 100644 index 0f3bf39..0000000 --- a/NOICommunityLib/Sources/ArticlesClientLive/Live.swift +++ /dev/null @@ -1,102 +0,0 @@ -// SPDX-FileCopyrightText: NOI Techpark -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -// -// Live.swift -// ArticlesClientLive -// -// Created by Matteo Matassoni on 10/05/22. -// - -import Foundation -import Combine -import Core -import ArticlesClient - -// MARK: - Private Constants - -private let baseURL = URL(string: "https://tourism.opendatahub.com")! -private let articlesJsonDecoder: JSONDecoder = { - let jsonDecoder = JSONDecoder() - - jsonDecoder.dateDecodingStrategy = .custom { decoder in - let container = try decoder.singleValueContainer() - let dateStr = try container.decode(String.self) - - let dateFormatter = DateFormatter() - dateFormatter.calendar = Calendar(identifier: .iso8601) - dateFormatter.timeZone = TimeZone(identifier: "Europe/Rome") - dateFormatter.locale = Locale(identifier: "en_US_POSIX") - - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZZZ" - if let date = dateFormatter.date(from: dateStr) { - return date - } - - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZZZ" - if let date = dateFormatter.date(from: dateStr) { - return date - } - - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" - if let date = dateFormatter.date(from: dateStr) { - return date - } - - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - if let date = dateFormatter.date(from: dateStr) { - return date - } - - throw DecodingError.dataCorruptedError( - in: container, - debugDescription: "Cannot decode date string \(dateStr)" - ) - } - - jsonDecoder.keyDecodingStrategy = .convertFromPascalCase - return jsonDecoder -}() - -// MARK: - ArticlesClient+Live - -extension ArticlesClient { - - public static func live(urlSession: URLSession = .shared) -> Self { - Self( - list: { startDate, publishedon, pageSize, pageNumber in - let urlRequest = Endpoint.articleList( - startDate: startDate, - publishedon: publishedon, - pageSize: pageSize, - pageNumber: pageNumber - ).makeRequest(withBaseURL: baseURL) - - return urlSession - .dataTaskPublisher(for: urlRequest) - .map { data, response in data } - .decode( - type: MyArticleListResponse.self, - decoder: articlesJsonDecoder - ) - .map(ArticleListResponse.init(from:)) - .eraseToAnyPublisher() - }, - detail: { id in - let urlRequest = Endpoint.article(id: id) - .makeRequest(withBaseURL: baseURL) - - return urlSession - .dataTaskPublisher(for: urlRequest) - .map { data, response in data } - .decode( - type: Article.self, - decoder: articlesJsonDecoder - ) - .eraseToAnyPublisher() - } - ) - } - -} diff --git a/NOICommunityLib/Sources/ArticlesClientLive/Models.swift b/NOICommunityLib/Sources/ArticlesClientLive/Models.swift deleted file mode 100644 index b0347b1..0000000 --- a/NOICommunityLib/Sources/ArticlesClientLive/Models.swift +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-FileCopyrightText: NOI Techpark -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -// -// Models.swift -// ArticlesClient -// -// Created by Matteo Matassoni on 10/05/22. -// - -import Foundation -import ArticlesClient - -// MARK: - ArticleListResponse - -struct MyArticleListResponse: Codable, Equatable { - - let totalResults: Int - - let totalPages: Int - - let currentPage: Int - - let previousPageURL: URL? - - let nextPageURL: URL? - - let items: [Article]? - - private enum CodingKeys: String, CodingKey { - case totalResults - case totalPages - case currentPage - case previousPageURL = "previousPage" - case nextPageURL = "nextPage" - case items - } - -} - -private func extractPageNumber(url: URL) -> Int? { - let urlComponents = URLComponents( - url: url, - resolvingAgainstBaseURL: true - ) - return urlComponents? - .queryItems? - .first { $0.name == "pagenumber"}? - .value - .flatMap(Int.init) -} - -extension ArticleListResponse { - - init(from response: MyArticleListResponse) { - self.init( - totalResults: response.totalResults, - totalPages: response.totalPages, - currentPage: response.currentPage, - previousPage: response.previousPageURL.flatMap(extractPageNumber(url:)), - nextPage: response.nextPageURL.flatMap(extractPageNumber(url:)), - items: response.items - ) - } - -} diff --git a/NOICommunityLib/Sources/EventShortClient/Implementations/EventShortClientImplementation.swift b/NOICommunityLib/Sources/EventShortClient/Implementations/EventShortClientImplementation.swift index a8e3cd4..fdfd49b 100644 --- a/NOICommunityLib/Sources/EventShortClient/Implementations/EventShortClientImplementation.swift +++ b/NOICommunityLib/Sources/EventShortClient/Implementations/EventShortClientImplementation.swift @@ -149,7 +149,7 @@ public final class EventShortClientImplementation: EventShortClient { optimizeDates: Bool?, fields: [String]?, removeNullValues: Bool? - ) async throws -> EventShort { + ) async throws -> EventShort { let request = Endpoint .eventShort( id: id,