Skip to content

Commit c7f0dd7

Browse files
authored
FEATURE: Create User Session (#152)
* WIP * WIP * FEATURE: Create User Session * Fix failing Linux build * Fix lint error
1 parent 97cfb00 commit c7f0dd7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+675
-85
lines changed

.github/workflows/codeql.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ env:
2222
jobs:
2323
analyze:
2424
name: Analyze
25-
runs-on: macos-13
25+
runs-on: macos-14
2626
timeout-minutes: 120
2727
permissions:
2828
actions: read

Sources/TMDb/Networking/HTTPClient/URLSessionHTTPClientAdapter.swift renamed to Sources/TMDb/Adapters/URLSessionHTTPClientAdapter.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,11 @@ final class URLSessionHTTPClientAdapter: HTTPClient {
3030
self.urlSession = urlSession
3131
}
3232

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//
2+
// JSONEncoder+TMDb.swift
3+
// TMDb
4+
//
5+
// Copyright © 2024 Adam Young.
6+
//
7+
// Licensed under the Apache License, Version 2.0 (the "License");
8+
// you may not use this file except in compliance with the License.
9+
// You may obtain a copy of the License at
10+
//
11+
// http://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing, software
14+
// distributed under the License is distributed on an AS IS BASIS,
15+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
// See the License for the specific language governing permissions and
17+
// limitations under the License.
18+
//
19+
20+
import Foundation
21+
22+
extension JSONEncoder {
23+
24+
static var theMovieDatabase: JSONEncoder {
25+
let encoder = JSONEncoder()
26+
encoder.keyEncodingStrategy = .convertToSnakeCase
27+
encoder.dateEncodingStrategy = .formatted(.theMovieDatabase)
28+
return encoder
29+
}
30+
31+
static var theMovieDatabaseAuth: JSONEncoder {
32+
let encoder = JSONEncoder()
33+
encoder.keyEncodingStrategy = .convertToSnakeCase
34+
encoder.dateEncodingStrategy = .formatted(.theMovieDatabaseAuth)
35+
return encoder
36+
}
37+
38+
}

Sources/TMDb/Models/Session.swift

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
//
2+
// Session.swift
3+
// TMDb
4+
//
5+
// Copyright © 2024 Adam Young.
6+
//
7+
// Licensed under the Apache License, Version 2.0 (the "License");
8+
// you may not use this file except in compliance with the License.
9+
// You may obtain a copy of the License at
10+
//
11+
// http://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing, software
14+
// distributed under the License is distributed on an AS IS BASIS,
15+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
// See the License for the specific language governing permissions and
17+
// limitations under the License.
18+
//
19+
20+
import Foundation
21+
22+
///
23+
/// A model representing a TMDb session.
24+
///
25+
public struct Session: Codable, Equatable, Hashable {
26+
27+
///
28+
/// Was session creation successful.
29+
///
30+
public let success: Bool
31+
32+
///
33+
/// The session identifier.
34+
///
35+
public let sessionID: String
36+
37+
///
38+
/// Creates a TMDb session object..
39+
///
40+
/// - Parameters:
41+
/// - success: Was token creation successful.
42+
/// - sessionID: The session identifier.
43+
///
44+
public init(success: Bool, sessionID: String) {
45+
self.success = success
46+
self.sessionID = sessionID
47+
}
48+
49+
}
50+
51+
extension Session {
52+
53+
private enum CodingKeys: String, CodingKey {
54+
case success
55+
case sessionID = "sessionId"
56+
}
57+
58+
}

Sources/TMDb/Models/TMDbError+TMDbAPIError.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ extension TMDbError {
3131
case .notFound:
3232
self = .notFound
3333

34+
case let .unauthorised(message):
35+
self = .unauthorised(message)
36+
3437
case let .network(error):
3538
self = .network(error)
3639

Sources/TMDb/Models/TMDbError.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ public enum TMDbError: Equatable, LocalizedError {
2424
/// An error indicating the resource could not be found.
2525
case notFound
2626

27+
case unauthorised(String?)
28+
2729
/// An error indicating there was a network problem.
2830
case network(Error)
2931

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

40+
case let (.unauthorised(lhsMessage), .unauthorised(rhsMessage)):
41+
lhsMessage == rhsMessage
42+
3843
case (.network, .network):
3944
true
4045

@@ -56,6 +61,9 @@ public extension TMDbError {
5661
case .notFound:
5762
"Not found"
5863

64+
case .unauthorised:
65+
"Unauthorised"
66+
5967
case .network:
6068
"Network error"
6169

Sources/TMDb/Models/Token.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public struct Token: Codable, Equatable, Hashable {
4040
public let expiresAt: Date
4141

4242
///
43-
/// Creates an internediate request token.
43+
/// Creates an internediate request token object.
4444
///
4545
/// - Parameters:
4646
/// - success: Was token creation successful.

Sources/TMDb/Networking/APIClient/HTTPClient.swift renamed to Sources/TMDb/Networking/HTTPClient.swift

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,13 @@ import Foundation
2525
public protocol HTTPClient {
2626

2727
///
28-
/// Performs an HTTP GET request.
28+
/// Performs an HTTP request.
2929
///
3030
/// - Parameters:
31-
/// - url: The URL to use for the request.
32-
/// - headers: Additional HTTP headers to use in the request.
31+
/// - request: The HTTP request.
3332
///
3433
/// - Returns: An HTTP response object.
3534
///
36-
func get(url: URL, headers: [String: String]) async throws -> HTTPResponse
35+
func perform(request: HTTPRequest) async throws -> HTTPResponse
3736

3837
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//
2+
// HTTPRequest.swift
3+
// TMDb
4+
//
5+
// Copyright © 2024 Adam Young.
6+
//
7+
// Licensed under the Apache License, Version 2.0 (the "License");
8+
// you may not use this file except in compliance with the License.
9+
// You may obtain a copy of the License at
10+
//
11+
// http://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing, software
14+
// distributed under the License is distributed on an AS IS BASIS,
15+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
// See the License for the specific language governing permissions and
17+
// limitations under the License.
18+
//
19+
20+
import Foundation
21+
22+
public struct HTTPRequest {
23+
24+
public let url: URL
25+
public let method: HTTPRequest.Method
26+
public let headers: [String: String]
27+
public let body: Data?
28+
29+
public init(
30+
url: URL,
31+
method: HTTPRequest.Method = .get,
32+
headers: [String: String] = [:],
33+
body: Data? = nil
34+
) {
35+
self.url = url
36+
self.method = method
37+
self.headers = headers
38+
self.body = body
39+
}
40+
41+
}
42+
43+
public extension HTTPRequest {
44+
45+
enum Method: String {
46+
case get = "GET"
47+
case post = "POST"
48+
}
49+
50+
}

Sources/TMDb/Networking/APIClient/Serialiser.swift renamed to Sources/TMDb/Networking/Serialiser.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,19 @@ import Foundation
2222
actor Serialiser {
2323

2424
private let decoder: JSONDecoder
25+
private let encoder: JSONEncoder
2526

26-
init(decoder: JSONDecoder) {
27+
init(decoder: JSONDecoder, encoder: JSONEncoder) {
2728
self.decoder = decoder
29+
self.encoder = encoder
2830
}
2931

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

36+
func encode(_ value: some Encodable) async throws -> Data {
37+
try encoder.encode(value)
38+
}
39+
3440
}

Sources/TMDb/Networking/APIClient/TMDbAPIClient.swift renamed to Sources/TMDb/Networking/TMDbAPIClient.swift

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,34 +47,49 @@ final class TMDbAPIClient: APIClient {
4747
"Accept": "application/json"
4848
]
4949

50-
let response: HTTPResponse
50+
let request = HTTPRequest(url: url, headers: headers)
51+
let responseObject: Response = try await perform(request: request)
52+
53+
return responseObject
54+
}
5155

56+
func post<Response: Decodable>(path: URL, body: some Encodable) async throws -> Response {
57+
let url = urlFromPath(path)
58+
let headers = [
59+
"Content-Type": "application/json",
60+
"Accept": "application/json"
61+
]
62+
let data: Data
5263
do {
53-
response = try await httpClient.get(url: url, headers: headers)
64+
data = try await serialiser.encode(body)
5465
} catch let error {
55-
throw TMDbAPIError.network(error)
66+
throw TMDbAPIError.encode(error)
5667
}
5768

58-
try await validate(response: response)
69+
let request = HTTPRequest(url: url, method: .post, headers: headers, body: data)
70+
let responseObject: Response = try await perform(request: request)
5971

60-
guard let data = response.data else {
61-
throw TMDbAPIError.unknown
62-
}
72+
return responseObject
73+
}
74+
75+
}
76+
77+
extension TMDbAPIClient {
78+
79+
private func perform<Response: Decodable>(request: HTTPRequest) async throws -> Response {
80+
let response: HTTPResponse
6381

64-
let decodedResponse: Response
6582
do {
66-
decodedResponse = try await serialiser.decode(Response.self, from: data)
83+
response = try await httpClient.perform(request: request)
6784
} catch let error {
68-
throw TMDbAPIError.decode(error)
85+
throw TMDbAPIError.network(error)
6986
}
7087

88+
let decodedResponse: Response = try await decodeResponse(response: response)
89+
7190
return decodedResponse
7291
}
7392

74-
}
75-
76-
extension TMDbAPIClient {
77-
7893
private func urlFromPath(_ path: URL) -> URL {
7994
guard var urlComponents = URLComponents(url: path, resolvingAgainstBaseURL: true) else {
8095
return path
@@ -89,6 +104,23 @@ extension TMDbAPIClient {
89104
.appendingLanguage(localeProvider.languageCode)
90105
}
91106

107+
private func decodeResponse<Response: Decodable>(response: HTTPResponse) async throws -> Response {
108+
try await validate(response: response)
109+
110+
guard let data = response.data else {
111+
throw TMDbAPIError.unknown
112+
}
113+
114+
let decodedResponse: Response
115+
do {
116+
decodedResponse = try await serialiser.decode(Response.self, from: data)
117+
} catch let error {
118+
throw TMDbAPIError.decode(error)
119+
}
120+
121+
return decodedResponse
122+
}
123+
92124
private func validate(response: HTTPResponse) async throws {
93125
let statusCode = response.statusCode
94126
if (200 ... 299).contains(statusCode) {

Sources/TMDb/Networking/APIClient/TMDbAPIError.swift renamed to Sources/TMDb/Networking/TMDbAPIError.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ enum TMDbAPIError: Error {
9494
///
9595
case gatewayTimeout(String?)
9696

97+
///
98+
/// Data encode error.
99+
///
100+
case encode(Error)
101+
97102
///
98103
/// Data decode error.
99104
///

Sources/TMDb/Services/APIClient.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ protocol APIClient {
2323

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

26+
func post<Body: Encodable, Response: Decodable>(path: URL, body: Body) async throws -> Response
27+
2628
}
2729

2830
extension APIClient {
@@ -31,4 +33,8 @@ extension APIClient {
3133
try await get(path: endpoint.path)
3234
}
3335

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

Sources/TMDb/Services/Authentication/AuthenticationEndpoint.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ enum AuthenticationEndpoint {
2323

2424
case createGuestSession
2525
case createRequestToken
26+
case createSession
2627

2728
}
2829

@@ -41,6 +42,11 @@ extension AuthenticationEndpoint: Endpoint {
4142
Self.basePath
4243
.appendingPathComponent("token")
4344
.appendingPathComponent("new")
45+
46+
case .createSession:
47+
Self.basePath
48+
.appendingPathComponent("session")
49+
.appendingPathComponent("new")
4450
}
4551
}
4652

0 commit comments

Comments
 (0)