[Library] iTunes library (#5)
This PR contains the work done to implement the `iTunesService` service that fetches the reviews from the **Apple App Store**. Reviewed-on: rock-n-code/app-reviews#5 Co-authored-by: Javier Cicchelli <javier@rock-n-code.com> Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
This commit was merged in pull request #5.
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// 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 ReviewsFoundationKit
|
||||
import ReviewsFeedKit
|
||||
|
||||
extension ServiceConfiguration {
|
||||
|
||||
// MARK: Initialisers
|
||||
init(session: URLSessionConfiguration = .ephemeral) {
|
||||
self.init(
|
||||
host: .iTunes,
|
||||
session: session,
|
||||
decoder: .default
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
//
|
||||
// Review+Codable.swift
|
||||
// ReviewsiTunesKit
|
||||
//
|
||||
// Created by Javier Cicchelli on 17/03/2024.
|
||||
// Copyright © 2024 Röck+Cöde VoF. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ReviewsFeedKit
|
||||
|
||||
// MARK: - Decodable
|
||||
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)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user