Skip to content

Commit

Permalink
FEATURE: Create User Session (#152)
Browse files Browse the repository at this point in the history
* WIP

* WIP

* FEATURE: Create User Session

* Fix failing Linux build

* Fix lint error
  • Loading branch information
adamayoung authored Feb 6, 2024
1 parent 97cfb00 commit c7f0dd7
Show file tree
Hide file tree
Showing 43 changed files with 675 additions and 85 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ env:
jobs:
analyze:
name: Analyze
runs-on: macos-13
runs-on: macos-14
timeout-minutes: 120
permissions:
actions: read
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ final class URLSessionHTTPClientAdapter: HTTPClient {
self.urlSession = urlSession
}

func get(url: URL, headers: [String: String]) async throws -> HTTPResponse {
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "GET"
for header in headers {
func perform(request: HTTPRequest) async throws -> HTTPResponse {
var urlRequest = URLRequest(url: request.url)
urlRequest.httpMethod = request.method.rawValue
urlRequest.httpBody = request.body
for header in request.headers {
urlRequest.addValue(header.value, forHTTPHeaderField: header.key)
}

Expand Down
38 changes: 38 additions & 0 deletions Sources/TMDb/Extensions/JSONEncoder+TMDb.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// JSONEncoder+TMDb.swift
// TMDb
//
// Copyright © 2024 Adam Young.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an AS IS BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation

extension JSONEncoder {

static var theMovieDatabase: JSONEncoder {
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.dateEncodingStrategy = .formatted(.theMovieDatabase)
return encoder
}

static var theMovieDatabaseAuth: JSONEncoder {
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.dateEncodingStrategy = .formatted(.theMovieDatabaseAuth)
return encoder
}

}
58 changes: 58 additions & 0 deletions Sources/TMDb/Models/Session.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//
// Session.swift
// TMDb
//
// Copyright © 2024 Adam Young.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an AS IS BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation

///
/// A model representing a TMDb session.
///
public struct Session: Codable, Equatable, Hashable {

///
/// Was session creation successful.
///
public let success: Bool

///
/// The session identifier.
///
public let sessionID: String

///
/// Creates a TMDb session object..
///
/// - Parameters:
/// - success: Was token creation successful.
/// - sessionID: The session identifier.
///
public init(success: Bool, sessionID: String) {
self.success = success
self.sessionID = sessionID
}

}

extension Session {

private enum CodingKeys: String, CodingKey {
case success
case sessionID = "sessionId"
}

}
3 changes: 3 additions & 0 deletions Sources/TMDb/Models/TMDbError+TMDbAPIError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ extension TMDbError {
case .notFound:
self = .notFound

case let .unauthorised(message):
self = .unauthorised(message)

case let .network(error):
self = .network(error)

Expand Down
8 changes: 8 additions & 0 deletions Sources/TMDb/Models/TMDbError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ public enum TMDbError: Equatable, LocalizedError {
/// An error indicating the resource could not be found.
case notFound

case unauthorised(String?)

/// An error indicating there was a network problem.
case network(Error)

Expand All @@ -35,6 +37,9 @@ public enum TMDbError: Equatable, LocalizedError {
case (.notFound, .notFound):
true

case let (.unauthorised(lhsMessage), .unauthorised(rhsMessage)):
lhsMessage == rhsMessage

case (.network, .network):
true

Expand All @@ -56,6 +61,9 @@ public extension TMDbError {
case .notFound:
"Not found"

case .unauthorised:
"Unauthorised"

case .network:
"Network error"

Expand Down
2 changes: 1 addition & 1 deletion Sources/TMDb/Models/Token.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public struct Token: Codable, Equatable, Hashable {
public let expiresAt: Date

///
/// Creates an internediate request token.
/// Creates an internediate request token object.
///
/// - Parameters:
/// - success: Was token creation successful.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,13 @@ import Foundation
public protocol HTTPClient {

///
/// Performs an HTTP GET request.
/// Performs an HTTP request.
///
/// - Parameters:
/// - url: The URL to use for the request.
/// - headers: Additional HTTP headers to use in the request.
/// - request: The HTTP request.
///
/// - Returns: An HTTP response object.
///
func get(url: URL, headers: [String: String]) async throws -> HTTPResponse
func perform(request: HTTPRequest) async throws -> HTTPResponse

}
50 changes: 50 additions & 0 deletions Sources/TMDb/Networking/HTTPRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// HTTPRequest.swift
// TMDb
//
// Copyright © 2024 Adam Young.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an AS IS BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation

public struct HTTPRequest {

public let url: URL
public let method: HTTPRequest.Method
public let headers: [String: String]
public let body: Data?

public init(
url: URL,
method: HTTPRequest.Method = .get,
headers: [String: String] = [:],
body: Data? = nil
) {
self.url = url
self.method = method
self.headers = headers
self.body = body
}

}

public extension HTTPRequest {

enum Method: String {
case get = "GET"
case post = "POST"
}

}
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,19 @@ import Foundation
actor Serialiser {

private let decoder: JSONDecoder
private let encoder: JSONEncoder

init(decoder: JSONDecoder) {
init(decoder: JSONDecoder, encoder: JSONEncoder) {
self.decoder = decoder
self.encoder = encoder
}

func decode<T: Decodable>(_ type: T.Type, from data: Data) async throws -> T {
try decoder.decode(type, from: data)
}

func encode(_ value: some Encodable) async throws -> Data {
try encoder.encode(value)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -47,34 +47,49 @@ final class TMDbAPIClient: APIClient {
"Accept": "application/json"
]

let response: HTTPResponse
let request = HTTPRequest(url: url, headers: headers)
let responseObject: Response = try await perform(request: request)

return responseObject
}

func post<Response: Decodable>(path: URL, body: some Encodable) async throws -> Response {
let url = urlFromPath(path)
let headers = [
"Content-Type": "application/json",
"Accept": "application/json"
]
let data: Data
do {
response = try await httpClient.get(url: url, headers: headers)
data = try await serialiser.encode(body)
} catch let error {
throw TMDbAPIError.network(error)
throw TMDbAPIError.encode(error)
}

try await validate(response: response)
let request = HTTPRequest(url: url, method: .post, headers: headers, body: data)
let responseObject: Response = try await perform(request: request)

guard let data = response.data else {
throw TMDbAPIError.unknown
}
return responseObject
}

}

extension TMDbAPIClient {

private func perform<Response: Decodable>(request: HTTPRequest) async throws -> Response {
let response: HTTPResponse

let decodedResponse: Response
do {
decodedResponse = try await serialiser.decode(Response.self, from: data)
response = try await httpClient.perform(request: request)
} catch let error {
throw TMDbAPIError.decode(error)
throw TMDbAPIError.network(error)
}

let decodedResponse: Response = try await decodeResponse(response: response)

return decodedResponse
}

}

extension TMDbAPIClient {

private func urlFromPath(_ path: URL) -> URL {
guard var urlComponents = URLComponents(url: path, resolvingAgainstBaseURL: true) else {
return path
Expand All @@ -89,6 +104,23 @@ extension TMDbAPIClient {
.appendingLanguage(localeProvider.languageCode)
}

private func decodeResponse<Response: Decodable>(response: HTTPResponse) async throws -> Response {
try await validate(response: response)

guard let data = response.data else {
throw TMDbAPIError.unknown
}

let decodedResponse: Response
do {
decodedResponse = try await serialiser.decode(Response.self, from: data)
} catch let error {
throw TMDbAPIError.decode(error)
}

return decodedResponse
}

private func validate(response: HTTPResponse) async throws {
let statusCode = response.statusCode
if (200 ... 299).contains(statusCode) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ enum TMDbAPIError: Error {
///
case gatewayTimeout(String?)

///
/// Data encode error.
///
case encode(Error)

///
/// Data decode error.
///
Expand Down
6 changes: 6 additions & 0 deletions Sources/TMDb/Services/APIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ protocol APIClient {

func get<Response: Decodable>(path: URL) async throws -> Response

func post<Body: Encodable, Response: Decodable>(path: URL, body: Body) async throws -> Response

}

extension APIClient {
Expand All @@ -31,4 +33,8 @@ extension APIClient {
try await get(path: endpoint.path)
}

func post<Response: Decodable>(endpoint: Endpoint, body: some Encodable) async throws -> Response {
try await post(path: endpoint.path, body: body)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ enum AuthenticationEndpoint {

case createGuestSession
case createRequestToken
case createSession

}

Expand All @@ -41,6 +42,11 @@ extension AuthenticationEndpoint: Endpoint {
Self.basePath
.appendingPathComponent("token")
.appendingPathComponent("new")

case .createSession:
Self.basePath
.appendingPathComponent("session")
.appendingPathComponent("new")
}
}

Expand Down
Loading

0 comments on commit c7f0dd7

Please sign in to comment.