Skip to content

Commit

Permalink
Update openapi spec (#382)
Browse files Browse the repository at this point in the history
  • Loading branch information
etoledom authored Sep 23, 2024
1 parent b571ff2 commit 5eae547
Show file tree
Hide file tree
Showing 26 changed files with 766 additions and 144 deletions.
14 changes: 12 additions & 2 deletions Sources/Gravatar/Network/Errors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ public enum ResponseErrorReason: Sendable {
case URLSessionError(error: Error)

/// The response contains an invalid HTTP status code. By default, status code >= 400 is recognized as invalid.
case invalidHTTPStatusCode(response: HTTPURLResponse)
case invalidHTTPStatusCode(response: HTTPURLResponse, data: Data)

/// The response is not a `HTTPURLResponse`.
case invalidURLResponse(response: URLResponse)

///
case invalidRequest(error: ModelError)

/// An unexpected error has occurred.
case unexpected(Error)

Expand All @@ -24,12 +27,19 @@ public enum ResponseErrorReason: Sendable {

// If self is a `.invalidHTTPStatusCode` returns the HTTP statusCode from the response. Otherwise returns `nil`.
public var httpStatusCode: Int? {
if case .invalidHTTPStatusCode(let response) = self {
if case .invalidHTTPStatusCode(let response, _) = self {
return response.statusCode
}
return nil
}

public var errorData: Data? {
if case .invalidHTTPStatusCode(_, let data) = self {
return data
}
return nil
}

public var cancelled: Bool {
if case .URLSessionError(let error) = self {
return (error as NSError).code == -999
Expand Down
16 changes: 13 additions & 3 deletions Sources/Gravatar/Network/Services/AvatarService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,21 @@ public struct AvatarService: Sendable {
/// - Returns: An asynchronously-delivered `AvatarModel` instance, containing data of the newly created avatar.
@discardableResult
public func upload(_ image: UIImage, accessToken: String) async throws -> Avatar {
let (data, _) = try await imageUploader.uploadImage(image, accessToken: accessToken, additionalHTTPHeaders: [(name: "Client-Type", value: "ios")])
do {
return try data.decode(keyDecodingStrategy: .convertFromSnakeCase)
} catch {
let (data, _) = try await imageUploader.uploadImage(image, accessToken: accessToken, additionalHTTPHeaders: [(name: "Client-Type", value: "ios")])
return try data.decode()

} catch ImageUploadError.responseError(reason: let reason) where reason.httpStatusCode == 400 {
guard let data = reason.errorData, let error: ModelError = try? data.decode() else {
throw ImageUploadError.responseError(reason: reason)
}
throw error
} catch let error as DecodingError {
throw ImageUploadError.responseError(reason: .unexpected(error))
} catch {
throw error
}
}
}

extension ModelError: Error {}
63 changes: 20 additions & 43 deletions Sources/Gravatar/Network/Services/ProfileService.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import Foundation

private let baseURL = URL(string: "https://api.gravatar.com/v3/profiles/")!
private let avatarsBaseURL = URL(string: "https://api.gravatar.com/v3/me/avatars")!
private let identitiesBaseURL = "https://api.gravatar.com/v3/me/identities/"
private let avatarsBaseURLComponents = URLComponents(string: "https://api.gravatar.com/v3/me/avatars")!

private func selectAvatarBaseURL(with profileID: ProfileIdentifier) -> URL? {
URL(string: "https://api.gravatar.com/v3/me/identities/\(profileID.id)/avatar")
private func selectAvatarBaseURL(with avatarID: String) -> URL? {
URL(string: "https://api.gravatar.com/v3/me/avatars/\(avatarID)/email")
}

/// A service to perform Profile related tasks.
Expand All @@ -30,41 +29,30 @@ public struct ProfileService: ProfileFetching, Sendable {
return try await fetch(with: request)
}

package func fetchAvatars(with token: String) async throws -> [Avatar] {
package func fetchAvatars(with token: String, id: ProfileIdentifier) async throws -> [Avatar] {
do {
let url = avatarsBaseURL
guard let url = avatarsBaseURLComponents.settingQueryItems([.init(name: "selected_email", value: id.id)]).url else {
throw APIError.requestError(reason: .urlInitializationFailed)
}
let request = URLRequest(url: url).settingAuthorizationHeaderField(with: token)
let (data, _) = try await client.fetchData(with: request)
return try data.decode(keyDecodingStrategy: .convertFromSnakeCase)
return try data.decode()
} catch {
throw error.apiError()
}
}

package func fetchIdentity(token: String, profileID: ProfileIdentifier) async throws -> ProfileIdentity {
guard let url = URL(string: identitiesBaseURL + profileID.id) else {
throw APIError.requestError(reason: .urlInitializationFailed)
}
do {
let request = URLRequest(url: url).settingAuthorizationHeaderField(with: token)
let (data, _) = try await client.fetchData(with: request)
return try data.decode(keyDecodingStrategy: .convertFromSnakeCase)
} catch {
throw error.apiError()
}
}

package func selectAvatar(token: String, profileID: ProfileIdentifier, avatarID: String) async throws -> ProfileIdentity {
guard let url = selectAvatarBaseURL(with: profileID) else {
package func selectAvatar(token: String, profileID: ProfileIdentifier, avatarID: String) async throws -> Avatar {
guard let url = selectAvatarBaseURL(with: avatarID) else {
throw APIError.requestError(reason: .urlInitializationFailed)
}

do {
var request = URLRequest(url: url).settingAuthorizationHeaderField(with: token)
request.httpMethod = "POST"
request.httpBody = try SelectAvatarBody(avatarId: avatarID).data
request.httpBody = try SelectAvatarBody(emailHash: profileID.id).data
let (data, _) = try await client.fetchData(with: request)
return try data.decode(keyDecodingStrategy: .convertFromSnakeCase)
return try data.decode()
} catch {
throw error.apiError()
}
Expand Down Expand Up @@ -97,36 +85,25 @@ extension URLRequest {
}
}

package struct ProfileIdentity: Decodable, Sendable {
package let emailHash: String
package let rating: String
package let imageId: String
package let imageUrl: String
}

public struct Avatar: Decodable, Sendable {
private let imageId: String
private let imageUrl: String

package init(id: String, url: String) {
self.imageId = id
self.imageUrl = url
}

extension Avatar {
public var id: String {
imageId
}

public var url: String {
imageUrl
}

public var isSelected: Bool {
selected == true
}
}

private struct SelectAvatarBody: Encodable, Sendable {
private let avatarId: String
private let emailHash: String

init(avatarId: String) {
self.avatarId = avatarId
init(emailHash: String) {
self.emailHash = emailHash
}

var data: Data {
Expand Down
14 changes: 7 additions & 7 deletions Sources/Gravatar/Network/Services/URLSessionHTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Foundation

/// Common errors for all HTTP operations.
enum HTTPClientError: Error {
case invalidHTTPStatusCodeError(HTTPURLResponse)
case invalidHTTPStatusCodeError(HTTPURLResponse, Data)
case invalidURLResponseError(URLResponse)
case URLSessionError(Error)
}
Expand All @@ -21,7 +21,7 @@ struct URLSessionHTTPClient: HTTPClient {
} catch {
throw HTTPClientError.URLSessionError(error)
}
let httpResponse = try validatedHTTPResponse(result.response)
let httpResponse = try validatedHTTPResponse(result.response, data: result.data)
return (result.data, httpResponse)
}

Expand All @@ -32,7 +32,7 @@ struct URLSessionHTTPClient: HTTPClient {
} catch {
throw HTTPClientError.URLSessionError(error)
}
return try (result.data, validatedHTTPResponse(result.response))
return try (result.data, validatedHTTPResponse(result.response, data: result.data))
}
}

Expand All @@ -44,12 +44,12 @@ extension URLRequest {
}
}

private func validatedHTTPResponse(_ response: URLResponse) throws -> HTTPURLResponse {
private func validatedHTTPResponse(_ response: URLResponse, data: Data) throws -> HTTPURLResponse {
guard let httpResponse = response as? HTTPURLResponse else {
throw HTTPClientError.invalidURLResponseError(response)
}
if isErrorResponse(httpResponse) {
throw HTTPClientError.invalidHTTPStatusCodeError(httpResponse)
throw HTTPClientError.invalidHTTPStatusCodeError(httpResponse, data)
}
return httpResponse
}
Expand All @@ -63,8 +63,8 @@ extension HTTPClientError {
switch self {
case .URLSessionError(let error):
.URLSessionError(error: error)
case .invalidHTTPStatusCodeError(let response):
.invalidHTTPStatusCode(response: response)
case .invalidHTTPStatusCodeError(let response, let data):
.invalidHTTPStatusCode(response: response, data: data)
case .invalidURLResponseError(let response):
.invalidURLResponse(response: response)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Foundation

public struct AssociatedEmail200Response: Codable, Hashable, Sendable {
/// Whether the email is associated with a Gravatar account.
public private(set) var associated: Bool

@available(*, deprecated, message: "init will become internal on the next release")
public init(associated: Bool) {
self.associated = associated
}

@available(*, deprecated, message: "CodingKeys will become internal on the next release.")
public enum CodingKeys: String, CodingKey, CaseIterable {
case associated
}

enum InternalCodingKeys: String, CodingKey, CaseIterable {
case associated
}

// Encodable protocol methods

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: InternalCodingKeys.self)
try container.encode(associated, forKey: .associated)
}

// Decodable protocol methods

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: InternalCodingKeys.self)

associated = try container.decode(Bool.self, forKey: .associated)
}
}
79 changes: 79 additions & 0 deletions Sources/Gravatar/OpenApi/Generated/Avatar.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import Foundation

/// An avatar that the user has already uploaded to their Gravatar account.
///
public struct Avatar: Codable, Hashable, Sendable {
public enum Rating: String, Codable, CaseIterable, Sendable {
case g = "G"
case pg = "PG"
case r = "R"
case x = "X"
}

/// Unique identifier for the image.
public private(set) var imageId: String
/// Image URL
public private(set) var imageUrl: String
/// Rating associated with the image.
public private(set) var rating: Rating
/// Date and time when the image was last updated.
public private(set) var updatedDate: Date
/// Alternative text description of the image.
public private(set) var altText: String
/// Whether the image is currently selected as the provided selected email's avatar.
public private(set) var selected: Bool?

@available(*, deprecated, message: "init will become internal on the next release")
public init(imageId: String, imageUrl: String, rating: Rating, updatedDate: Date, altText: String, selected: Bool? = nil) {
self.imageId = imageId
self.imageUrl = imageUrl
self.rating = rating
self.updatedDate = updatedDate
self.altText = altText
self.selected = selected
}

@available(*, deprecated, message: "CodingKeys will become internal on the next release.")
public enum CodingKeys: String, CodingKey, CaseIterable {
case imageId = "image_id"
case imageUrl = "image_url"
case rating
case updatedDate = "updated_date"
case altText = "alt_text"
case selected
}

enum InternalCodingKeys: String, CodingKey, CaseIterable {
case imageId = "image_id"
case imageUrl = "image_url"
case rating
case updatedDate = "updated_date"
case altText = "alt_text"
case selected
}

// Encodable protocol methods

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: InternalCodingKeys.self)
try container.encode(imageId, forKey: .imageId)
try container.encode(imageUrl, forKey: .imageUrl)
try container.encode(rating, forKey: .rating)
try container.encode(updatedDate, forKey: .updatedDate)
try container.encode(altText, forKey: .altText)
try container.encodeIfPresent(selected, forKey: .selected)
}

// Decodable protocol methods

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: InternalCodingKeys.self)

imageId = try container.decode(String.self, forKey: .imageId)
imageUrl = try container.decode(String.self, forKey: .imageUrl)
rating = try container.decode(Rating.self, forKey: .rating)
updatedDate = try container.decode(Date.self, forKey: .updatedDate)
altText = try container.decode(String.self, forKey: .altText)
selected = try container.decodeIfPresent(Bool.self, forKey: .selected)
}
}
Loading

0 comments on commit 5eae547

Please sign in to comment.