From b57d40e277718556a59c4b300bf19825bcc23f8d Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 16 Mar 2024 03:03:22 +0100 Subject: [PATCH 01/24] Fixed the reference to the Libraries test plan for the schemes in the project. --- Reviews.xcodeproj/project.pbxproj | 10 +++++----- .../Libraries.xctestplan | 0 2 files changed, 5 insertions(+), 5 deletions(-) rename Libraries.xctestplan => Test Plans/Libraries.xctestplan (100%) diff --git a/Reviews.xcodeproj/project.pbxproj b/Reviews.xcodeproj/project.pbxproj index 4feecf0..822b22a 100644 --- a/Reviews.xcodeproj/project.pbxproj +++ b/Reviews.xcodeproj/project.pbxproj @@ -46,7 +46,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 02900C492BA530E6008D2E8D /* Libraries.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = Libraries.xctestplan; sourceTree = ""; }; + 02900C4B2BA5347A008D2E8D /* Libraries.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = Libraries.xctestplan; sourceTree = ""; }; 02DC7F8F2BA51793000EEEBE /* ReviewsFeed.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ReviewsFeed.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 02DC7F912BA51793000EEEBE /* ReviewsFeed.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ReviewsFeed.h; sourceTree = ""; }; 02DC7FB12BA52084000EEEBE /* Libraries */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Libraries; sourceTree = ""; }; @@ -82,12 +82,12 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 02900C4A2BA53162008D2E8D /* Test Plans */ = { + 02900C4C2BA5347A008D2E8D /* Test Plans */ = { isa = PBXGroup; children = ( - 02900C492BA530E6008D2E8D /* Libraries.xctestplan */, + 02900C4B2BA5347A008D2E8D /* Libraries.xctestplan */, ); - name = "Test Plans"; + path = "Test Plans"; sourceTree = ""; }; 02DC7F722BA4F8F0000EEEBE /* Resources */ = { @@ -166,7 +166,7 @@ 02DC7FB12BA52084000EEEBE /* Libraries */, 02DC7FAB2BA51848000EEEBE /* Frameworks */, 345AD11A24C6EDD9004E2EE1 /* App */, - 02900C4A2BA53162008D2E8D /* Test Plans */, + 02900C4C2BA5347A008D2E8D /* Test Plans */, 345AD11924C6EDD9004E2EE1 /* Products */, ); sourceTree = ""; diff --git a/Libraries.xctestplan b/Test Plans/Libraries.xctestplan similarity index 100% rename from Libraries.xctestplan rename to Test Plans/Libraries.xctestplan -- 2.47.1 From 6d6de392b32cf4fd978faf6e4f47120f9d241763 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 16 Mar 2024 03:06:57 +0100 Subject: [PATCH 02/24] Created the API library in the Libraries package. --- Libraries/Package.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Libraries/Package.swift b/Libraries/Package.swift index 2fa99db..1746f4f 100644 --- a/Libraries/Package.swift +++ b/Libraries/Package.swift @@ -11,15 +11,27 @@ let package = Package( .library( name: .Product.name.kit, targets: [ + .Target.api, .Target.foundation ] ), ], targets: [ + .target( + name: .Target.api, + path: "API/Kit" + ), .target( name: .Target.foundation, path: "Foundation/Kit" ), + .testTarget( + name: .Target.api.test, + dependencies: [ + .byName(name: .Target.api) + ], + path: "API/Test" + ), .testTarget( name: .Target.foundation.test, dependencies: [ @@ -41,6 +53,7 @@ private extension String { } enum Target { + static let api = "\(String.Product.name)API" static let foundation = "\(String.Product.name)Foundation" } } -- 2.47.1 From 8f279d203428260fa6ad228d3a1bf03c933d112a Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 16 Mar 2024 09:40:16 +0100 Subject: [PATCH 03/24] Moved the existing files under the Feed framework to its "Bundle" folder. --- Frameworks/Feed/{ => Bundle}/ReviewsFeed.h | 0 .../Sources/DetailsViewController.swift | 0 .../Sources/FeedViewController.swift | 0 .../Feed/{ => Bundle}/Sources/Review.swift | 0 .../{ => Bundle}/Sources/ReviewCell.swift | 0 Reviews.xcodeproj/project.pbxproj | 20 +++++++++++++++++-- 6 files changed, 18 insertions(+), 2 deletions(-) rename Frameworks/Feed/{ => Bundle}/ReviewsFeed.h (100%) rename Frameworks/Feed/{ => Bundle}/Sources/DetailsViewController.swift (100%) rename Frameworks/Feed/{ => Bundle}/Sources/FeedViewController.swift (100%) rename Frameworks/Feed/{ => Bundle}/Sources/Review.swift (100%) rename Frameworks/Feed/{ => Bundle}/Sources/ReviewCell.swift (100%) diff --git a/Frameworks/Feed/ReviewsFeed.h b/Frameworks/Feed/Bundle/ReviewsFeed.h similarity index 100% rename from Frameworks/Feed/ReviewsFeed.h rename to Frameworks/Feed/Bundle/ReviewsFeed.h diff --git a/Frameworks/Feed/Sources/DetailsViewController.swift b/Frameworks/Feed/Bundle/Sources/DetailsViewController.swift similarity index 100% rename from Frameworks/Feed/Sources/DetailsViewController.swift rename to Frameworks/Feed/Bundle/Sources/DetailsViewController.swift diff --git a/Frameworks/Feed/Sources/FeedViewController.swift b/Frameworks/Feed/Bundle/Sources/FeedViewController.swift similarity index 100% rename from Frameworks/Feed/Sources/FeedViewController.swift rename to Frameworks/Feed/Bundle/Sources/FeedViewController.swift diff --git a/Frameworks/Feed/Sources/Review.swift b/Frameworks/Feed/Bundle/Sources/Review.swift similarity index 100% rename from Frameworks/Feed/Sources/Review.swift rename to Frameworks/Feed/Bundle/Sources/Review.swift diff --git a/Frameworks/Feed/Sources/ReviewCell.swift b/Frameworks/Feed/Bundle/Sources/ReviewCell.swift similarity index 100% rename from Frameworks/Feed/Sources/ReviewCell.swift rename to Frameworks/Feed/Bundle/Sources/ReviewCell.swift diff --git a/Reviews.xcodeproj/project.pbxproj b/Reviews.xcodeproj/project.pbxproj index 822b22a..22420ed 100644 --- a/Reviews.xcodeproj/project.pbxproj +++ b/Reviews.xcodeproj/project.pbxproj @@ -90,6 +90,22 @@ path = "Test Plans"; sourceTree = ""; }; + 02A6DA2F2BA591C000B943E2 /* Bundle */ = { + isa = PBXGroup; + children = ( + 02DC7F912BA51793000EEEBE /* ReviewsFeed.h */, + 02DC7FB02BA51B4F000EEEBE /* Sources */, + ); + path = Bundle; + sourceTree = ""; + }; + 02A6DA302BA5929F00B943E2 /* Test */ = { + isa = PBXGroup; + children = ( + ); + path = Test; + sourceTree = ""; + }; 02DC7F722BA4F8F0000EEEBE /* Resources */ = { isa = PBXGroup; children = ( @@ -135,8 +151,8 @@ 02DC7F902BA51793000EEEBE /* Feed */ = { isa = PBXGroup; children = ( - 02DC7F912BA51793000EEEBE /* ReviewsFeed.h */, - 02DC7FB02BA51B4F000EEEBE /* Sources */, + 02A6DA2F2BA591C000B943E2 /* Bundle */, + 02A6DA302BA5929F00B943E2 /* Test */, ); path = Feed; sourceTree = ""; -- 2.47.1 From 8dacbfd51d31a2f96909c05d65dceb37fe6121a6 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 17 Mar 2024 15:53:08 +0100 Subject: [PATCH 04/24] Created the Feed and iTunes libraries in the Libraries package. --- Libraries/Package.swift | 42 ++++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/Libraries/Package.swift b/Libraries/Package.swift index 1746f4f..25f97f7 100644 --- a/Libraries/Package.swift +++ b/Libraries/Package.swift @@ -11,34 +11,53 @@ let package = Package( .library( name: .Product.name.kit, targets: [ - .Target.api, - .Target.foundation + .Target.feed.kit, + .Target.foundation.kit, + .Target.iTunes.kit, ] ), ], targets: [ .target( - name: .Target.api, - path: "API/Kit" + name: .Target.feed.kit, + dependencies: [ + .byName(name: .Target.foundation.kit), + ], + path: "Feed/Kit" ), .target( - name: .Target.foundation, + name: .Target.foundation.kit, path: "Foundation/Kit" ), - .testTarget( - name: .Target.api.test, + .target( + name: .Target.iTunes.kit, dependencies: [ - .byName(name: .Target.api) + .byName(name: .Target.feed.kit), + .byName(name: .Target.foundation.kit), ], - path: "API/Test" + path: "iTunes/Kit" + ), + .testTarget( + name: .Target.feed.test, + dependencies: [ + .byName(name: .Target.feed.kit), + ], + path: "Feed/Test" ), .testTarget( name: .Target.foundation.test, dependencies: [ - .byName(name: .Target.foundation) + .byName(name: .Target.foundation.kit), ], path: "Foundation/Test" ), + .testTarget( + name: .Target.iTunes.test, + dependencies: [ + .byName(name: .Target.iTunes.kit), + ], + path: "iTunes/Test" + ), ] ) @@ -53,8 +72,9 @@ private extension String { } enum Target { - static let api = "\(String.Product.name)API" + static let feed = "\(String.Product.name)Feed" static let foundation = "\(String.Product.name)Foundation" + static let iTunes = "\(String.Product.name)iTunes" } } -- 2.47.1 From 6245e87a983d7aa0f0b7eb3484fbeb78556cf33c Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 17 Mar 2024 16:07:08 +0100 Subject: [PATCH 05/24] Defined the Endpoint protocol in the Feed library. --- .../Feed/Kit/Sources/Protocols/Endpoint.swift | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 Libraries/Feed/Kit/Sources/Protocols/Endpoint.swift diff --git a/Libraries/Feed/Kit/Sources/Protocols/Endpoint.swift b/Libraries/Feed/Kit/Sources/Protocols/Endpoint.swift new file mode 100644 index 0000000..44f59f2 --- /dev/null +++ b/Libraries/Feed/Kit/Sources/Protocols/Endpoint.swift @@ -0,0 +1,32 @@ +// +// Endpoint.swift +// ReviewsFeedKit +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import Foundation + +public protocol Endpoint { + + // MARK: Associated types + associatedtype Input: EndpointInput + associatedtype Output: EndpointOutput + + // MARK: Properties + var host: URL { get } + var decoder: JSONDecoder { get } + var session: URLSession { get } + + // MARK: Functions + func callAsFunction(_ input: Input) async throws -> Output + func makePath(with input: Input) throws -> String + +} + +// MARK: - Input +public protocol EndpointInput {} + +// MARK: - Output +public protocol EndpointOutput {} -- 2.47.1 From 7a7df4cfeaac3ef85283c517678740ca35ccd714 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 17 Mar 2024 16:08:18 +0100 Subject: [PATCH 06/24] Implemented the Review model in the Feed library. --- .../Feed/Kit/Sources/Models/Review.swift | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 Libraries/Feed/Kit/Sources/Models/Review.swift diff --git a/Libraries/Feed/Kit/Sources/Models/Review.swift b/Libraries/Feed/Kit/Sources/Models/Review.swift new file mode 100644 index 0000000..90b511e --- /dev/null +++ b/Libraries/Feed/Kit/Sources/Models/Review.swift @@ -0,0 +1,41 @@ +// +// Review.swift +// ReviewsFeedKit +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import Foundation + +public struct Review { + + // MARK: Constants + public let author: String + public let content: String + public let id: Int + public let rating: Int + public let title: String + public let updated: Date + public let version: String + + // MARK: Initialisers + public init( + id: Int, + author: String, + title: String, + content: String, + rating: Int, + version: String, + updated: Date + ) { + self.author = author + self.content = content + self.id = id + self.rating = rating + self.title = title + self.updated = updated + self.version = version + } + +} -- 2.47.1 From 93a52fd93e744c7909cd275bb4f8e67903367649 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 17 Mar 2024 16:08:51 +0100 Subject: [PATCH 07/24] Defined the GetReviewsEndpoint protocol in the Feed library. --- .../Endpoints/GetReviewsEndpoint.swift | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 Libraries/Feed/Kit/Sources/Protocols/Endpoints/GetReviewsEndpoint.swift diff --git a/Libraries/Feed/Kit/Sources/Protocols/Endpoints/GetReviewsEndpoint.swift b/Libraries/Feed/Kit/Sources/Protocols/Endpoints/GetReviewsEndpoint.swift new file mode 100644 index 0000000..0159e5e --- /dev/null +++ b/Libraries/Feed/Kit/Sources/Protocols/Endpoints/GetReviewsEndpoint.swift @@ -0,0 +1,44 @@ +// +// GetReviewsEndpoint.swift +// ReviewsFeedKit +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import Foundation + +public protocol GetReviewsEndpoint: Endpoint +where Input == GetReviewsInput, + Output == GetReviewsOutput {} + +// MARK: - Input +public struct GetReviewsInput: EndpointInput { + + // MARK: Constants + public let appID: String + public let countryCode: String + + // MARK: Initialisers + public init( + appID: String, + countryCode: String + ) { + self.appID = appID + self.countryCode = countryCode + } + +} + +// MARK: - Output +public struct GetReviewsOutput: EndpointOutput { + + // MARK: Constants + public let reviews: [Review] + + // MARK: Initialisers + public init(reviews: [Review]) { + self.reviews = reviews + } + +} -- 2.47.1 From f97025fdb82c2accb6c061a0e86c758d7eb7bde7 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 17 Mar 2024 16:09:13 +0100 Subject: [PATCH 08/24] Defined the EndpointError error in the Feed library. --- .../Feed/Kit/Sources/Errors/EndpointError.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 Libraries/Feed/Kit/Sources/Errors/EndpointError.swift diff --git a/Libraries/Feed/Kit/Sources/Errors/EndpointError.swift b/Libraries/Feed/Kit/Sources/Errors/EndpointError.swift new file mode 100644 index 0000000..d8e4249 --- /dev/null +++ b/Libraries/Feed/Kit/Sources/Errors/EndpointError.swift @@ -0,0 +1,13 @@ +// +// EndpointError.swift +// ReviewsFeedKit +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +public enum EndpointError: Error { + case inputParametersEmpty + case requestFailed(statusCode: Int) + case responseNotFound +} -- 2.47.1 From 7beb36068b32a34c90444bc636b9b5b2a73d63e1 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 17 Mar 2024 16:10:13 +0100 Subject: [PATCH 09/24] Defined the Service protocol in the Feed library. --- .../Feed/Kit/Sources/Protocols/Service.swift | 21 ++++++++++++++ .../Structs/ServiceConfiguration.swift | 29 +++++++++++++++++++ .../Sources/Extensions/String+Constants.swift | 2 +- 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 Libraries/Feed/Kit/Sources/Protocols/Service.swift create mode 100644 Libraries/Feed/Kit/Sources/Structs/ServiceConfiguration.swift diff --git a/Libraries/Feed/Kit/Sources/Protocols/Service.swift b/Libraries/Feed/Kit/Sources/Protocols/Service.swift new file mode 100644 index 0000000..d602442 --- /dev/null +++ b/Libraries/Feed/Kit/Sources/Protocols/Service.swift @@ -0,0 +1,21 @@ +// +// Service.swift +// ReviewsFeedKit +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import Foundation + +public protocol Service { + + // MARK: Properties + var configuration: ServiceConfiguration { get } + var decoder: JSONDecoder { get } + var session: URLSession { get } + + // MARK: Functions + func getReviews(_ input: GetReviewsInput) async throws -> GetReviewsOutput + +} diff --git a/Libraries/Feed/Kit/Sources/Structs/ServiceConfiguration.swift b/Libraries/Feed/Kit/Sources/Structs/ServiceConfiguration.swift new file mode 100644 index 0000000..32df28f --- /dev/null +++ b/Libraries/Feed/Kit/Sources/Structs/ServiceConfiguration.swift @@ -0,0 +1,29 @@ +// +// ServiceConfiguration.swift +// ReviewsiTunesKit +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import Foundation + +public struct ServiceConfiguration { + + // MARK: Constants + public let decoder: JSONDecoder + public let host: URL + public let session: URLSessionConfiguration + + // MARK: Initialisers + public init( + host: URL, + session: URLSessionConfiguration = .ephemeral, + decoder: JSONDecoder? = nil + ) { + self.decoder = decoder ?? .init() + self.host = host + self.session = session + } + +} diff --git a/Libraries/Foundation/Kit/Sources/Extensions/String+Constants.swift b/Libraries/Foundation/Kit/Sources/Extensions/String+Constants.swift index ab80392..f6d84ec 100644 --- a/Libraries/Foundation/Kit/Sources/Extensions/String+Constants.swift +++ b/Libraries/Foundation/Kit/Sources/Extensions/String+Constants.swift @@ -1,6 +1,6 @@ // // String+Constants.swift -// ReviewsFoundation +// ReviewsFoundationKit // // Created by Javier Cicchelli on 16/03/2024. // Copyright © 2024 Röck+Cöde VoF. All rights reserved. -- 2.47.1 From ab81347b22617d5a9e79430e8a4598d7b8a95548 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 17 Mar 2024 16:38:47 +0100 Subject: [PATCH 10/24] Implemented the "bitBucket" static constant for the URL+Constants extension in the Foundation library. --- .../Sources/Extensions/URL+Constants.swift | 29 +++++++++++++++++++ .../Extensions/String+ConstantsTests.swift | 4 +-- .../Tests/Extensions/URL+ConstantsTests.swift | 27 +++++++++++++++++ 3 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 Libraries/Foundation/Kit/Sources/Extensions/URL+Constants.swift create mode 100644 Libraries/Foundation/Test/Tests/Extensions/URL+ConstantsTests.swift diff --git a/Libraries/Foundation/Kit/Sources/Extensions/URL+Constants.swift b/Libraries/Foundation/Kit/Sources/Extensions/URL+Constants.swift new file mode 100644 index 0000000..24bb24c --- /dev/null +++ b/Libraries/Foundation/Kit/Sources/Extensions/URL+Constants.swift @@ -0,0 +1,29 @@ +// +// URL+Constants.swift +// ReviewsFoundationKit +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import Foundation + +extension URL { + + // MARK: Constants + public static let bitBucket: URL = { + if #available(iOS 16.0, *) { + .init(filePath: .FilePath.bitBucket) + } else { + .init(fileURLWithPath: .FilePath.bitBucket) + } + }() + +} + +// MARK: - String+Constants +private extension String { + enum FilePath { + static let bitBucket = "/dev/null" + } +} diff --git a/Libraries/Foundation/Test/Tests/Extensions/String+ConstantsTests.swift b/Libraries/Foundation/Test/Tests/Extensions/String+ConstantsTests.swift index fcd80c7..cbbbbd0 100644 --- a/Libraries/Foundation/Test/Tests/Extensions/String+ConstantsTests.swift +++ b/Libraries/Foundation/Test/Tests/Extensions/String+ConstantsTests.swift @@ -6,7 +6,7 @@ // Copyright © 2024 Röck+Cöde VoF. All rights reserved. // -import ReviewsFoundation +import ReviewsFoundationKit import XCTest final class String_ConstantsTests: XCTestCase { @@ -15,7 +15,7 @@ final class String_ConstantsTests: XCTestCase { private var sut: String! // MARK: Constants tests - func testEmpty() throws { + func testEmpty() { // GIVEN sut = .empty diff --git a/Libraries/Foundation/Test/Tests/Extensions/URL+ConstantsTests.swift b/Libraries/Foundation/Test/Tests/Extensions/URL+ConstantsTests.swift new file mode 100644 index 0000000..91167b5 --- /dev/null +++ b/Libraries/Foundation/Test/Tests/Extensions/URL+ConstantsTests.swift @@ -0,0 +1,27 @@ +// +// URL+ConstantsTests.swift +// ReviewsFoundationTest +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import Foundation +import ReviewsFoundationKit +import XCTest + +final class URL_ConstantsTests: XCTestCase { + + // MARK: Properties + private var sut: URL! + + // MARK: Constants tests + func testBitBucket() { + sut = .bitBucket + + // WHEN + // THEN + XCTAssertEqual(sut.absoluteString, "file:///dev/null") + } + +} -- 2.47.1 From 54a546594df97484752d40e2d0d4c7eaea2e9e35 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 17 Mar 2024 16:44:32 +0100 Subject: [PATCH 11/24] Implemented the "init(session: )" initialiser for the ServiceConfiguration+Inits extension in the iTunes library. --- .../Extensions/JSONDecoder+Constants.swift | 22 ++++++++++++++++++ .../ServiceConfiguration+Inits.swift | 23 +++++++++++++++++++ .../iTunes/Kit/Extensions/URL+Constants.swift | 17 ++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 Libraries/iTunes/Kit/Extensions/JSONDecoder+Constants.swift create mode 100644 Libraries/iTunes/Kit/Extensions/ServiceConfiguration+Inits.swift create mode 100644 Libraries/iTunes/Kit/Extensions/URL+Constants.swift diff --git a/Libraries/iTunes/Kit/Extensions/JSONDecoder+Constants.swift b/Libraries/iTunes/Kit/Extensions/JSONDecoder+Constants.swift new file mode 100644 index 0000000..b5c5bed --- /dev/null +++ b/Libraries/iTunes/Kit/Extensions/JSONDecoder+Constants.swift @@ -0,0 +1,22 @@ +// +// JSONDecoder+Constants.swift +// ReviewsiTunesKit +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import Foundation + +extension JSONDecoder { + + // MARK: Constants + static let `default` = { + let decoder = JSONDecoder() + + decoder.dateDecodingStrategy = .iso8601 + + return decoder + }() + +} diff --git a/Libraries/iTunes/Kit/Extensions/ServiceConfiguration+Inits.swift b/Libraries/iTunes/Kit/Extensions/ServiceConfiguration+Inits.swift new file mode 100644 index 0000000..50ec996 --- /dev/null +++ b/Libraries/iTunes/Kit/Extensions/ServiceConfiguration+Inits.swift @@ -0,0 +1,23 @@ +// +// ServiceConfiguration+Inits.swift +// ReviewsiTunesKit +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import Foundation +import ReviewsFeedKit + +extension ServiceConfiguration { + + // MARK: Initialisers + init(session: URLSessionConfiguration = .ephemeral) { + self.init( + host: .iTunes, + session: session, + decoder: .default + ) + } + +} diff --git a/Libraries/iTunes/Kit/Extensions/URL+Constants.swift b/Libraries/iTunes/Kit/Extensions/URL+Constants.swift new file mode 100644 index 0000000..d989801 --- /dev/null +++ b/Libraries/iTunes/Kit/Extensions/URL+Constants.swift @@ -0,0 +1,17 @@ +// +// URL+Constants.swift +// ReviewsiTunesKit +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import Foundation +import ReviewsFoundationKit + +extension URL { + + // MARK: Constants + static let iTunes: URL = .init(string: "https://itunes.apple.com") ?? .bitBucket + +} -- 2.47.1 From 428b4dd88a9712ff71bf7107517274f69c481e42 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 17 Mar 2024 16:48:12 +0100 Subject: [PATCH 12/24] Conformed the Review model from the Feed library to the Decodable protocol in the iTunes library. --- .../Kit/Conformances/Review+Decodable.swift | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 Libraries/iTunes/Kit/Conformances/Review+Decodable.swift diff --git a/Libraries/iTunes/Kit/Conformances/Review+Decodable.swift b/Libraries/iTunes/Kit/Conformances/Review+Decodable.swift new file mode 100644 index 0000000..41269fe --- /dev/null +++ b/Libraries/iTunes/Kit/Conformances/Review+Decodable.swift @@ -0,0 +1,56 @@ +// +// Review.swift +// ReviewsiTunesKit +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import Foundation +import ReviewsFeedKit + +extension Review: Decodable { + + // MARK: Enumerations + enum ReviewKeys: String, CodingKey { + case author + case content + case id + case rating = "im:rating" + case title + case updated + case version = "im:version" + } + + enum NameKeys: String, CodingKey { + case name + } + + enum LabelKeys: String, CodingKey { + case label + } + + // MARK: Initialisers + public init(from decoder: any Decoder) throws { + let review = try decoder.container(keyedBy: ReviewKeys.self) + let authorName = try review.nestedContainer(keyedBy: NameKeys.self, forKey: .author) + let authorLabel = try authorName.nestedContainer(keyedBy: LabelKeys.self, forKey: .name) + let contentLabel = try review.nestedContainer(keyedBy: LabelKeys.self, forKey: .content) + let idLabel = try review.nestedContainer(keyedBy: LabelKeys.self, forKey: .id) + let ratingLabel = try review.nestedContainer(keyedBy: LabelKeys.self, forKey: .rating) + let titleLabel = try review.nestedContainer(keyedBy: LabelKeys.self, forKey: .title) + let versionLabel = try review.nestedContainer(keyedBy: LabelKeys.self, forKey: .version) + let updatedLabel = try review.nestedContainer(keyedBy: LabelKeys.self, forKey: .updated) + + self.init( + id: Int(try idLabel.decode(String.self, forKey: .label)) ?? 0, + author: try authorLabel.decode(String.self, forKey: .label), + title: try titleLabel.decode(String.self, forKey: .label), + content: try contentLabel.decode(String.self, forKey: .label), + rating: Int(try ratingLabel.decode(String.self, forKey: .label)) ?? 0, + version: try versionLabel.decode(String.self, forKey: .label), + updated: try updatedLabel.decode(Date.self, forKey: .label) + ) + } + +} -- 2.47.1 From 910fc0b612ff90d3edaad8c75eb2f4a7f4e5209d Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 17 Mar 2024 16:48:59 +0100 Subject: [PATCH 13/24] Implemented the Feed model in the iTunes library. --- Libraries/iTunes/Kit/Models/Feed.swift | 38 +++++++++++++++++++ .../Review+Decodable.swift | 0 2 files changed, 38 insertions(+) create mode 100644 Libraries/iTunes/Kit/Models/Feed.swift rename Libraries/iTunes/Kit/{Conformances => Models}/Review+Decodable.swift (100%) diff --git a/Libraries/iTunes/Kit/Models/Feed.swift b/Libraries/iTunes/Kit/Models/Feed.swift new file mode 100644 index 0000000..4928aef --- /dev/null +++ b/Libraries/iTunes/Kit/Models/Feed.swift @@ -0,0 +1,38 @@ +// +// Feed.swift +// ReviewsiTunesKit +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import ReviewsFeedKit + +struct Feed { + + // MARK: Constants + let entries: [Review] + +} + +// MARK: - Decodable +extension Feed: Decodable { + + // MARK: Enumerations + enum FeedKeys: String, CodingKey { + case feed + } + + enum EntryKeys: String, CodingKey { + case entry + } + + // MARK: Initialisers + init(from decoder: any Decoder) throws { + let feed = try decoder.container(keyedBy: FeedKeys.self) + let feedEntry = try feed.nestedContainer(keyedBy: EntryKeys.self, forKey: .feed) + + self.entries = try feedEntry.decode([Review].self, forKey: .entry) + } + +} diff --git a/Libraries/iTunes/Kit/Conformances/Review+Decodable.swift b/Libraries/iTunes/Kit/Models/Review+Decodable.swift similarity index 100% rename from Libraries/iTunes/Kit/Conformances/Review+Decodable.swift rename to Libraries/iTunes/Kit/Models/Review+Decodable.swift -- 2.47.1 From ef6a30d8c067e81f4a7a72ce46d66275b120ca70 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 17 Mar 2024 18:18:33 +0100 Subject: [PATCH 14/24] Implemented the MockURLProtocol class in the Foundation library. --- .../Kit/Sources/Classes/MockURLProtocol.swift | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 Libraries/Foundation/Kit/Sources/Classes/MockURLProtocol.swift diff --git a/Libraries/Foundation/Kit/Sources/Classes/MockURLProtocol.swift b/Libraries/Foundation/Kit/Sources/Classes/MockURLProtocol.swift new file mode 100644 index 0000000..805cc8d --- /dev/null +++ b/Libraries/Foundation/Kit/Sources/Classes/MockURLProtocol.swift @@ -0,0 +1,88 @@ +// +// MockURLProtocol.swift +// ReviewsFoundationKit +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import Foundation + +public class MockURLProtocol: URLProtocol { + + // MARK: Properties + public static var response: Response? + + // MARK: Functions + public override class func canInit(with task: URLSessionTask) -> Bool { + true + } + + public override class func canInit(with request: URLRequest) -> Bool { + true + } + + public override class func canonicalRequest(for request: URLRequest) -> URLRequest { + request + } + + public override func startLoading() { + guard + let url = request.url, + let response = Self.response + else { + client?.urlProtocolDidFinishLoading(self) + return + } + + if let data = try? response.data { + client?.urlProtocol(self, didLoad: data) + } + + if let httpResponse = HTTPURLResponse( + url: url, + statusCode: response.statusCode, + httpVersion: nil, + headerFields: nil + ) { + client?.urlProtocol( + self, + didReceive: httpResponse, + cacheStoragePolicy: .allowedInMemoryOnly + ) + } + + client?.urlProtocolDidFinishLoading(self) + } + + public override func stopLoading() {} + +} + +// MARK: - Structs + +extension MockURLProtocol { + public struct Response { + + // MARK: Constants + public let statusCode: Int + public let object: any Encodable + + // MARK: Initialisers + public init( + statusCode: Int, + object: any Codable + ) { + self.statusCode = statusCode + self.object = object + } + + // MARK: Computed + var data: Data? { + get throws { + try JSONEncoder().encode(object) + } + } + + } +} -- 2.47.1 From e10c1c9f59afffa1bff1a539353e50dd8fd623b4 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 17 Mar 2024 18:18:44 +0100 Subject: [PATCH 15/24] Implemented the "mock" static constant for the URLSessionConfiguration+Constants extension in the Foundation library. --- .../URLSessionConfiguration+Constants.swift | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 Libraries/Foundation/Kit/Sources/Extensions/URLSessionConfiguration+Constants.swift diff --git a/Libraries/Foundation/Kit/Sources/Extensions/URLSessionConfiguration+Constants.swift b/Libraries/Foundation/Kit/Sources/Extensions/URLSessionConfiguration+Constants.swift new file mode 100644 index 0000000..aa54f9a --- /dev/null +++ b/Libraries/Foundation/Kit/Sources/Extensions/URLSessionConfiguration+Constants.swift @@ -0,0 +1,22 @@ +// +// URLSessionConfiguration+Constants.swift +// ReviewsFoundationKit +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import Foundation + +extension URLSessionConfiguration { + + // MARK: Constants + public static let mock = { + let configuration: URLSessionConfiguration = .ephemeral + + configuration.protocolClasses = [MockURLProtocol.self] + + return configuration + }() + +} -- 2.47.1 From db6500589ac6048d1afa5ddbe00ed44b47412b05 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 17 Mar 2024 22:23:59 +0100 Subject: [PATCH 16/24] Moved the JSONDecoder+Constants extension from the iTunes library to the Foundation library. --- .../Kit/Sources}/Extensions/JSONDecoder+Constants.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename Libraries/{iTunes/Kit => Foundation/Kit/Sources}/Extensions/JSONDecoder+Constants.swift (91%) diff --git a/Libraries/iTunes/Kit/Extensions/JSONDecoder+Constants.swift b/Libraries/Foundation/Kit/Sources/Extensions/JSONDecoder+Constants.swift similarity index 91% rename from Libraries/iTunes/Kit/Extensions/JSONDecoder+Constants.swift rename to Libraries/Foundation/Kit/Sources/Extensions/JSONDecoder+Constants.swift index b5c5bed..a457872 100644 --- a/Libraries/iTunes/Kit/Extensions/JSONDecoder+Constants.swift +++ b/Libraries/Foundation/Kit/Sources/Extensions/JSONDecoder+Constants.swift @@ -11,7 +11,7 @@ import Foundation extension JSONDecoder { // MARK: Constants - static let `default` = { + public static let `default` = { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 -- 2.47.1 From 00b092f2b1ddc1ab8367d78c82f6000421f13235 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 17 Mar 2024 22:24:32 +0100 Subject: [PATCH 17/24] Implemented the "default" static constant for the JSONEncoder+Constants extension in the Foundation library. --- .../Extensions/JSONEncoder+Constants.swift | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 Libraries/Foundation/Kit/Sources/Extensions/JSONEncoder+Constants.swift diff --git a/Libraries/Foundation/Kit/Sources/Extensions/JSONEncoder+Constants.swift b/Libraries/Foundation/Kit/Sources/Extensions/JSONEncoder+Constants.swift new file mode 100644 index 0000000..ef713ab --- /dev/null +++ b/Libraries/Foundation/Kit/Sources/Extensions/JSONEncoder+Constants.swift @@ -0,0 +1,22 @@ +// +// JSONEncoder+Constants.swift +// ReviewsiTunesKit +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import Foundation + +extension JSONEncoder { + + // MARK: Constants + public static let `default` = { + let encoder = JSONEncoder() + + encoder.dateEncodingStrategy = .iso8601 + + return encoder + }() + +} -- 2.47.1 From 0e84dcde302c7d385807897bfd44e2b625d8bb92 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 17 Mar 2024 22:29:33 +0100 Subject: [PATCH 18/24] Conformed the Review model and the EndpointError error in the Feed library to the Equatable protocol. --- Libraries/Feed/Kit/Sources/Errors/EndpointError.swift | 2 +- Libraries/Feed/Kit/Sources/Models/Review.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Libraries/Feed/Kit/Sources/Errors/EndpointError.swift b/Libraries/Feed/Kit/Sources/Errors/EndpointError.swift index d8e4249..d19426d 100644 --- a/Libraries/Feed/Kit/Sources/Errors/EndpointError.swift +++ b/Libraries/Feed/Kit/Sources/Errors/EndpointError.swift @@ -6,7 +6,7 @@ // Copyright © 2024 Röck+Cöde VoF. All rights reserved. // -public enum EndpointError: Error { +public enum EndpointError: Error, Equatable { case inputParametersEmpty case requestFailed(statusCode: Int) case responseNotFound diff --git a/Libraries/Feed/Kit/Sources/Models/Review.swift b/Libraries/Feed/Kit/Sources/Models/Review.swift index 90b511e..d32a656 100644 --- a/Libraries/Feed/Kit/Sources/Models/Review.swift +++ b/Libraries/Feed/Kit/Sources/Models/Review.swift @@ -8,7 +8,7 @@ import Foundation -public struct Review { +public struct Review: Equatable { // MARK: Constants public let author: String -- 2.47.1 From ac1074324db74aa6df9a76002a49ad311fcbf949 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 17 Mar 2024 22:59:31 +0100 Subject: [PATCH 19/24] Implemented the "==(lhs: rhs: )" static function of the Equatable protocol for the Review model in the Feed library. --- .../Feed/Kit/Sources/Models/Review.swift | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/Libraries/Feed/Kit/Sources/Models/Review.swift b/Libraries/Feed/Kit/Sources/Models/Review.swift index d32a656..e1728b3 100644 --- a/Libraries/Feed/Kit/Sources/Models/Review.swift +++ b/Libraries/Feed/Kit/Sources/Models/Review.swift @@ -8,7 +8,7 @@ import Foundation -public struct Review: Equatable { +public struct Review { // MARK: Constants public let author: String @@ -39,3 +39,22 @@ public struct Review: Equatable { } } + +// MARK: - Equatable +extension Review: Equatable { + + // MARK: Functions + public static func == ( + lhs: Review, + rhs: Review + ) -> Bool { + lhs.author == rhs.author + && lhs.content == rhs.content + && lhs.id == rhs.id + && lhs.rating == rhs.rating + && lhs.title == rhs.title + && lhs.version == rhs.version + && lhs.updated.description == rhs.updated.description + } + +} -- 2.47.1 From f6a72e84654bfa129bf243585c009502304133a0 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 17 Mar 2024 23:00:30 +0100 Subject: [PATCH 20/24] Improved the MockURLProtocol class in the Foundation library to support optional object in its Response struct. --- .../Foundation/Kit/Sources/Classes/MockURLProtocol.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Libraries/Foundation/Kit/Sources/Classes/MockURLProtocol.swift b/Libraries/Foundation/Kit/Sources/Classes/MockURLProtocol.swift index 805cc8d..18cdb72 100644 --- a/Libraries/Foundation/Kit/Sources/Classes/MockURLProtocol.swift +++ b/Libraries/Foundation/Kit/Sources/Classes/MockURLProtocol.swift @@ -66,12 +66,12 @@ extension MockURLProtocol { // MARK: Constants public let statusCode: Int - public let object: any Encodable + public let object: (any Encodable)? // MARK: Initialisers public init( statusCode: Int, - object: any Codable + object: (any Codable)? = nil ) { self.statusCode = statusCode self.object = object @@ -80,7 +80,9 @@ extension MockURLProtocol { // MARK: Computed var data: Data? { get throws { - try JSONEncoder().encode(object) + guard let object else { return nil } + + return try JSONEncoder.default.encode(object) } } -- 2.47.1 From bd72c4fb7c4f05fa2b6d5bc3417d66bb8a0d2377 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 17 Mar 2024 23:01:47 +0100 Subject: [PATCH 21/24] Implemented the "encode(to: )" function of the Encodable protocol for the Feed model in the iTunes library. --- .../Kit/Extensions/ServiceConfiguration+Inits.swift | 1 + Libraries/iTunes/Kit/Models/Feed.swift | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/Libraries/iTunes/Kit/Extensions/ServiceConfiguration+Inits.swift b/Libraries/iTunes/Kit/Extensions/ServiceConfiguration+Inits.swift index 50ec996..f156e71 100644 --- a/Libraries/iTunes/Kit/Extensions/ServiceConfiguration+Inits.swift +++ b/Libraries/iTunes/Kit/Extensions/ServiceConfiguration+Inits.swift @@ -7,6 +7,7 @@ // import Foundation +import ReviewsFoundationKit import ReviewsFeedKit extension ServiceConfiguration { diff --git a/Libraries/iTunes/Kit/Models/Feed.swift b/Libraries/iTunes/Kit/Models/Feed.swift index 4928aef..718004c 100644 --- a/Libraries/iTunes/Kit/Models/Feed.swift +++ b/Libraries/iTunes/Kit/Models/Feed.swift @@ -36,3 +36,16 @@ extension Feed: Decodable { } } + +// MARK: - Encodable +extension Feed: Encodable { + + // MARK: Functions + func encode(to encoder: any Encoder) throws { + var feed = encoder.container(keyedBy: FeedKeys.self) + var feedEntry = feed.nestedContainer(keyedBy: EntryKeys.self, forKey: .feed) + + try feedEntry.encode(entries, forKey: .entry) + } + +} -- 2.47.1 From fc58ccf091958cf20aef93301bbeba8629c67071 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 17 Mar 2024 23:03:05 +0100 Subject: [PATCH 22/24] Implemented the "init(from: )" initialiser and the "encode(to: )" function of the Codable protocol for the Review+Codable extension in the iTunes library. --- ...w+Decodable.swift => Review+Codable.swift} | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) rename Libraries/iTunes/Kit/Models/{Review+Decodable.swift => Review+Codable.swift} (59%) diff --git a/Libraries/iTunes/Kit/Models/Review+Decodable.swift b/Libraries/iTunes/Kit/Models/Review+Codable.swift similarity index 59% rename from Libraries/iTunes/Kit/Models/Review+Decodable.swift rename to Libraries/iTunes/Kit/Models/Review+Codable.swift index 41269fe..c60640a 100644 --- a/Libraries/iTunes/Kit/Models/Review+Decodable.swift +++ b/Libraries/iTunes/Kit/Models/Review+Codable.swift @@ -1,5 +1,5 @@ // -// Review.swift +// Review+Codable.swift // ReviewsiTunesKit // // Created by Javier Cicchelli on 17/03/2024. @@ -9,6 +9,7 @@ import Foundation import ReviewsFeedKit +// MARK: - Decodable extension Review: Decodable { // MARK: Enumerations @@ -54,3 +55,30 @@ extension Review: Decodable { } } + +// MARK: - Encodable +extension Review: Encodable { + + // MARK: Functions + + public func encode(to encoder: any Encoder) throws { + var review = encoder.container(keyedBy: ReviewKeys.self) + var authorName = review.nestedContainer(keyedBy: NameKeys.self, forKey: .author) + var authorLabel = authorName.nestedContainer(keyedBy: LabelKeys.self, forKey: .name) + var contentLabel = review.nestedContainer(keyedBy: LabelKeys.self, forKey: .content) + var idLabel = review.nestedContainer(keyedBy: LabelKeys.self, forKey: .id) + var ratingLabel = review.nestedContainer(keyedBy: LabelKeys.self, forKey: .rating) + var titleLabel = review.nestedContainer(keyedBy: LabelKeys.self, forKey: .title) + var versionLabel = review.nestedContainer(keyedBy: LabelKeys.self, forKey: .version) + var updatedLabel = review.nestedContainer(keyedBy: LabelKeys.self, forKey: .updated) + + try authorLabel.encode(author, forKey: .label) + try contentLabel.encode(content, forKey: .label) + try idLabel.encode(String(id), forKey: .label) + try ratingLabel.encode(String(rating), forKey: .label) + try titleLabel.encode(title, forKey: .label) + try versionLabel.encode(version, forKey: .label) + try updatedLabel.encode(updated, forKey: .label) + } + +} -- 2.47.1 From 7b9a067713ea968d84237d04405015d92742f2b5 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 17 Mar 2024 23:13:13 +0100 Subject: [PATCH 23/24] Implemented the GetReviewsAPIEndpoint endpoint in the iTunes library. --- .../Kit/Endpoints/GetReviewsAPIEndpoint.swift | 70 ++++++ .../GetReviewsAPIEndpointTests.swift | 214 ++++++++++++++++++ 2 files changed, 284 insertions(+) create mode 100644 Libraries/iTunes/Kit/Endpoints/GetReviewsAPIEndpoint.swift create mode 100644 Libraries/iTunes/Test/Tests/Endpoints/GetReviewsAPIEndpointTests.swift diff --git a/Libraries/iTunes/Kit/Endpoints/GetReviewsAPIEndpoint.swift b/Libraries/iTunes/Kit/Endpoints/GetReviewsAPIEndpoint.swift new file mode 100644 index 0000000..1e8bdfb --- /dev/null +++ b/Libraries/iTunes/Kit/Endpoints/GetReviewsAPIEndpoint.swift @@ -0,0 +1,70 @@ +// +// GetReviewsAPIEndpoint.swift +// ReviewsiTunesKit +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import Foundation +import ReviewsFoundationKit +import ReviewsFeedKit + +struct GetReviewsAPIEndpoint: GetReviewsEndpoint { + + // MARK: Constants + let host: URL + let decoder: JSONDecoder + let session: URLSession + + // MARK: Functions + @discardableResult func callAsFunction( + _ input: ReviewsFeedKit.GetReviewsInput + ) async throws -> ReviewsFeedKit.GetReviewsOutput { + let path = try makePath(with: input) + let url = { + if #available(iOS 16.0, *) { + host.appending(path: path) + } else { + host.appendingPathComponent(path) + } + }() + + let (data, response) = try await session.data(from: url) + + guard let urlResponse = response as? HTTPURLResponse else { + throw EndpointError.responseNotFound + } + + guard urlResponse.statusCode == 200 else { + throw EndpointError.requestFailed(statusCode: urlResponse.statusCode) + } + + let feed = try decoder.decode(Feed.self, from: data) + + return .init(reviews: feed.entries) + } + + func makePath(with input: GetReviewsInput) throws -> String { + guard + !input.appID.isEmpty, + !input.countryCode.isEmpty + else{ + throw EndpointError.inputParametersEmpty + } + + return .init( + format: .Format.recentReviewsPath, + input.countryCode, + input.appID + ) + } + +} + +// MARK: - String+Formats +private extension String { + enum Format { + static let recentReviewsPath = "/%@/rss/customerreviews/id=%@/sortby=mostrecent/json" + } +} diff --git a/Libraries/iTunes/Test/Tests/Endpoints/GetReviewsAPIEndpointTests.swift b/Libraries/iTunes/Test/Tests/Endpoints/GetReviewsAPIEndpointTests.swift new file mode 100644 index 0000000..8090436 --- /dev/null +++ b/Libraries/iTunes/Test/Tests/Endpoints/GetReviewsAPIEndpointTests.swift @@ -0,0 +1,214 @@ +// +// GetReviewsAPIEndpointTests.swift +// ReviewsiTunesTest +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import ReviewsFeedKit +import ReviewsFoundationKit +import XCTest + +@testable import ReviewsiTunesKit + +final class GetReviewsAPIEndpointTests: XCTestCase { + + // MARK: Properties + private var input: GetReviewsAPIEndpoint.Input! + private var sut: GetReviewsAPIEndpoint! + + // MARK: Setup + override func setUp() async throws { + sut = .init( + host: .iTunes, + decoder: .default, + session: .init(configuration: .mock) + ) + } + + // MARK: Functions tests + func testCallAsFunction_whenResponseOK_withOneReview() async throws { + // GIVEN + let reviews: [Review] = .one + + input = .init( + appID: "1234567890", + countryCode: "abc" + ) + + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: reviews) + ) + + // WHEN + let output = try await sut(input) + + // THEN + XCTAssertFalse(output.reviews.isEmpty) + XCTAssertEqual(output.reviews.count, reviews.count) + XCTAssertEqual(output.reviews, reviews) + } + + func testCallAsFunction_whenResponseOK_withManyReviews() async throws { + // GIVEN + let reviews: [Review] = .many + + input = .init( + appID: "1234567890", + countryCode: "abc" + ) + + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: reviews) + ) + + // WHEN + let output = try await sut(input) + + // THEN + XCTAssertFalse(output.reviews.isEmpty) + XCTAssertEqual(output.reviews.count, reviews.count) + XCTAssertEqual(output.reviews, reviews) + } + + func testCallAsFunction_whenResponseOK_withEmptyReviews() async throws { + // GIVEN + let reviews: [Review] = .empty + + input = .init( + appID: "1234567890", + countryCode: "abc" + ) + + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: reviews) + ) + + // WHEN + let output = try await sut(input) + + // THEN + XCTAssertTrue(output.reviews.isEmpty) + XCTAssertEqual(output.reviews.count, reviews.count) + XCTAssertEqual(output.reviews, reviews) + } + + func testCallAsFunction_whenResponseNotOK() async throws { + // GIVEN + let statusCode = 404 + + input = .init( + appID: "1234567890", + countryCode: "abc" + ) + + MockURLProtocol.response = .init(statusCode: statusCode) + + // WHEN + // THEN + do { + try await sut(input) + } catch EndpointError.requestFailed(statusCode: statusCode) { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + + func testMakePath_withFilledInput() throws { + // GIVEN + input = .init( + appID: "1234567890", + countryCode: "abc" + ) + + // WHEN + let path = try sut.makePath(with: input) + + // THEN + XCTAssertEqual(path, "/\(input.countryCode)/rss/customerreviews/id=\(input.appID)/sortby=mostrecent/json") + } + + func testMakePath_withPartiallyFilledInput() throws { + // GIVEN + input = .init( + appID: .empty, + countryCode: "abc" + ) + + // WHEN + // THEN + XCTAssertThrowsError(try sut.makePath(with: input)) { error in + XCTAssertEqual(error as? EndpointError, .inputParametersEmpty) + } + } + + func testMakePath_withEmptyInput() throws { + // GIVEN + input = .init( + appID: .empty, + countryCode: .empty + ) + + // WHEN + // THEN + XCTAssertThrowsError(try sut.makePath(with: input)) { error in + XCTAssertEqual(error as? EndpointError, .inputParametersEmpty) + } + } + +} + +// MARK: - Array+Constants +private extension Array where Element == Review { + + // MARK: Constants + static let empty: [Review] = [] + + static let many: [Review] = [ + .init( + id: 1, + author: "Some author name #1 goes here...", + title: "Some title #1 goes here...", + content: "Some content #1 goes here...", + rating: 1, + version: "Some version #1 goes here...", + updated: .init() + ), + .init( + id: 2, + author: "Some author name #2 goes here...", + title: "Some title #2 goes here...", + content: "Some content #2 goes here...", + rating: 5, + version: "Some version #2 goes here...", + updated: .init() + ), + .init( + id: 3, + author: "Some author name #3 goes here...", + title: "Some title #3 goes here...", + content: "Some content #3 goes here...", + rating: 3, + version: "Some version #3 goes here...", + updated: .init() + ) + ] + + static let one: [Review] = [ + .init( + id: 1, + author: "Some author name goes here...", + title: "Some title goes here...", + content: "Some content goes here...", + rating: 3, + version: "Some version goes here...", + updated: .init() + ) + ] + +} -- 2.47.1 From 953f9241780c3a83d02bf9e4bf4b70a554065984 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 17 Mar 2024 23:45:55 +0100 Subject: [PATCH 24/24] Implemented the "getReviews(_: )" function for the iTunesService service in the iTunes library. --- .../iTunes/Kit/Services/iTunesService.swift | 43 +++++++ .../Helpers/Extensions/Array+Reviews.swift | 58 ++++++++++ .../GetReviewsAPIEndpointTests.swift | 53 --------- .../Tests/Services/iTunesServiceTests.swift | 108 ++++++++++++++++++ 4 files changed, 209 insertions(+), 53 deletions(-) create mode 100644 Libraries/iTunes/Kit/Services/iTunesService.swift create mode 100644 Libraries/iTunes/Test/Helpers/Extensions/Array+Reviews.swift create mode 100644 Libraries/iTunes/Test/Tests/Services/iTunesServiceTests.swift diff --git a/Libraries/iTunes/Kit/Services/iTunesService.swift b/Libraries/iTunes/Kit/Services/iTunesService.swift new file mode 100644 index 0000000..09f5c66 --- /dev/null +++ b/Libraries/iTunes/Kit/Services/iTunesService.swift @@ -0,0 +1,43 @@ +// +// iTunesService.swift +// ReviewsiTunes +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import Foundation +import ReviewsFeedKit + +public actor iTunesService: Service { + + // MARK: Constants + public let configuration: ServiceConfiguration + public let decoder: JSONDecoder + public let session: URLSession + + // MARK: Properties + private lazy var getReviewsEndpoint: GetReviewsAPIEndpoint = { + .init( + host: configuration.host, + decoder: decoder, + session: session + ) + }() + + // MARK: Initialisers + public init(configuration: ServiceConfiguration) { + self.configuration = configuration + self.decoder = configuration.decoder + self.session = .init(configuration: configuration.session) + } + + // MARK: Functions + @discardableResult public func getReviews( + _ input: GetReviewsInput + ) async throws -> GetReviewsOutput { + try await getReviewsEndpoint(input) + } + +} + diff --git a/Libraries/iTunes/Test/Helpers/Extensions/Array+Reviews.swift b/Libraries/iTunes/Test/Helpers/Extensions/Array+Reviews.swift new file mode 100644 index 0000000..5a0a419 --- /dev/null +++ b/Libraries/iTunes/Test/Helpers/Extensions/Array+Reviews.swift @@ -0,0 +1,58 @@ +// +// Array+Reviews.swift +// ReviewsiTunesTest +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import ReviewsFeedKit + +extension Array where Element == Review { + + // MARK: Constants + static let empty: [Review] = [] + + static let many: [Review] = [ + .init( + id: 1, + author: "Some author name #1 goes here...", + title: "Some title #1 goes here...", + content: "Some content #1 goes here...", + rating: 1, + version: "Some version #1 goes here...", + updated: .init() + ), + .init( + id: 2, + author: "Some author name #2 goes here...", + title: "Some title #2 goes here...", + content: "Some content #2 goes here...", + rating: 5, + version: "Some version #2 goes here...", + updated: .init() + ), + .init( + id: 3, + author: "Some author name #3 goes here...", + title: "Some title #3 goes here...", + content: "Some content #3 goes here...", + rating: 3, + version: "Some version #3 goes here...", + updated: .init() + ) + ] + + static let one: [Review] = [ + .init( + id: 1, + author: "Some author name goes here...", + title: "Some title goes here...", + content: "Some content goes here...", + rating: 3, + version: "Some version goes here...", + updated: .init() + ) + ] + +} diff --git a/Libraries/iTunes/Test/Tests/Endpoints/GetReviewsAPIEndpointTests.swift b/Libraries/iTunes/Test/Tests/Endpoints/GetReviewsAPIEndpointTests.swift index 8090436..a34ab03 100644 --- a/Libraries/iTunes/Test/Tests/Endpoints/GetReviewsAPIEndpointTests.swift +++ b/Libraries/iTunes/Test/Tests/Endpoints/GetReviewsAPIEndpointTests.swift @@ -47,7 +47,6 @@ final class GetReviewsAPIEndpointTests: XCTestCase { // THEN XCTAssertFalse(output.reviews.isEmpty) - XCTAssertEqual(output.reviews.count, reviews.count) XCTAssertEqual(output.reviews, reviews) } @@ -70,7 +69,6 @@ final class GetReviewsAPIEndpointTests: XCTestCase { // THEN XCTAssertFalse(output.reviews.isEmpty) - XCTAssertEqual(output.reviews.count, reviews.count) XCTAssertEqual(output.reviews, reviews) } @@ -93,7 +91,6 @@ final class GetReviewsAPIEndpointTests: XCTestCase { // THEN XCTAssertTrue(output.reviews.isEmpty) - XCTAssertEqual(output.reviews.count, reviews.count) XCTAssertEqual(output.reviews, reviews) } @@ -162,53 +159,3 @@ final class GetReviewsAPIEndpointTests: XCTestCase { } } - -// MARK: - Array+Constants -private extension Array where Element == Review { - - // MARK: Constants - static let empty: [Review] = [] - - static let many: [Review] = [ - .init( - id: 1, - author: "Some author name #1 goes here...", - title: "Some title #1 goes here...", - content: "Some content #1 goes here...", - rating: 1, - version: "Some version #1 goes here...", - updated: .init() - ), - .init( - id: 2, - author: "Some author name #2 goes here...", - title: "Some title #2 goes here...", - content: "Some content #2 goes here...", - rating: 5, - version: "Some version #2 goes here...", - updated: .init() - ), - .init( - id: 3, - author: "Some author name #3 goes here...", - title: "Some title #3 goes here...", - content: "Some content #3 goes here...", - rating: 3, - version: "Some version #3 goes here...", - updated: .init() - ) - ] - - static let one: [Review] = [ - .init( - id: 1, - author: "Some author name goes here...", - title: "Some title goes here...", - content: "Some content goes here...", - rating: 3, - version: "Some version goes here...", - updated: .init() - ) - ] - -} diff --git a/Libraries/iTunes/Test/Tests/Services/iTunesServiceTests.swift b/Libraries/iTunes/Test/Tests/Services/iTunesServiceTests.swift new file mode 100644 index 0000000..c52ec9b --- /dev/null +++ b/Libraries/iTunes/Test/Tests/Services/iTunesServiceTests.swift @@ -0,0 +1,108 @@ +// +// iTunesServiceTests.swift +// ReviewsiTunesTest +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import ReviewsFeedKit +import ReviewsFoundationKit +import XCTest + +@testable import ReviewsiTunesKit + +final class iTunesServiceTests: XCTestCase { + + // MARK: Properties + private var sut: iTunesService! + + // MARK: Setup + override func setUp() async throws { + sut = .init(configuration: .init(session: .mock)) + } + + // MARK: Functions + func testGetReviews_whenResponseOK_withOneReview() async throws { + // GIVEN + let reviews: [Review] = .one + + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: reviews) + ) + + // WHEN + let output = try await sut.getReviews(.init( + appID: "1234567890", + countryCode: "abc" + )) + + // THEN + XCTAssertFalse(output.reviews.isEmpty) + XCTAssertEqual(output.reviews.count, reviews.count) + XCTAssertEqual(output.reviews, reviews) + } + + func testGetReviews_whenResponseOK_withManyReview() async throws { + // GIVEN + let reviews: [Review] = .many + + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: reviews) + ) + + // WHEN + let output = try await sut.getReviews(.init( + appID: "1234567890", + countryCode: "abc" + )) + + // THEN + XCTAssertFalse(output.reviews.isEmpty) + XCTAssertEqual(output.reviews.count, reviews.count) + XCTAssertEqual(output.reviews, reviews) + } + + func testGetReviews_whenResponseOK_withEmptyReview() async throws { + // GIVEN + let reviews: [Review] = .empty + + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: reviews) + ) + + // WHEN + let output = try await sut.getReviews(.init( + appID: "1234567890", + countryCode: "abc" + )) + + // THEN + XCTAssertTrue(output.reviews.isEmpty) + XCTAssertEqual(output.reviews, reviews) + } + + func testGetReviews_whenResponseNotOK() async throws { + // GIVEN + let statusCode = 404 + + MockURLProtocol.response = .init(statusCode: statusCode) + + // WHEN + // THEN + do { + try await sut.getReviews(.init( + appID: "1234567890", + countryCode: "abc" + )) + } catch EndpointError.requestFailed(statusCode: statusCode) { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + +} -- 2.47.1