Skip to content

Commit 1747b63

Browse files
authored
Documentation (#9)
* Add basic GET function * Create network/request protocols and implementations (#2) * Create basic protocols and implementations Added network and request protocols as well as base implementations for each. * Condense enum + abstract URLRequest * Convert URLSession to stored variable * Abstract configuration into enum * feature/request-body-handling: Configurable request body (#3) * Allow for data model in body * Add default content-type header * Add retry and timeout logic (#4) * Add basic caching (#5) * epic/cookie-management: Implement Cookie Management Operations and Request Configuration (#6) * Implement Cookie Management * Refactor into CookieManager * Refactor Request + Private UD * Add Request cookie method * Implement batch requests (#7) * Add docstrings + inline comments
1 parent c3151cf commit 1747b63

9 files changed

+861
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//
2+
// CacheConfiguration.swift
3+
//
4+
//
5+
// Created by Sam Gilmore on 7/19/24.
6+
//
7+
8+
import Foundation
9+
10+
/// Configuration options for caching network responses.
11+
public struct CacheConfiguration {
12+
/// The memory capacity of the cache, in bytes.
13+
public var memoryCapacity: Int
14+
15+
/// The disk capacity of the cache, in bytes.
16+
public var diskCapacity: Int
17+
18+
/// The path for the disk cache storage.
19+
public var diskPath: String?
20+
21+
/// The cache policy for the request.
22+
public var cachePolicy: URLRequest.CachePolicy
23+
24+
/// Initializes a new cache configuration with the provided parameters.
25+
///
26+
/// - Parameters:
27+
/// - memoryCapacity: The memory capacity of the cache, in bytes. Default is 20 MB.
28+
/// - diskCapacity: The disk capacity of the cache, in bytes. Default is 100 MB.
29+
/// - diskPath: The path for the disk cache storage. Default is `nil`.
30+
/// - cachePolicy: The cache policy for the request. Default is `.useProtocolCachePolicy`.
31+
public init(memoryCapacity: Int = 20 * 1024 * 1024, // 20 MB
32+
diskCapacity: Int = 100 * 1024 * 1024, // 100 MB
33+
diskPath: String? = nil,
34+
cachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy) {
35+
self.memoryCapacity = memoryCapacity
36+
self.diskCapacity = diskCapacity
37+
self.diskPath = diskPath
38+
self.cachePolicy = cachePolicy
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
//
2+
// CookieManager.swift
3+
//
4+
//
5+
// Created by Sam Gilmore on 7/22/24.
6+
//
7+
8+
import Foundation
9+
10+
/// Manages cookies for network requests and responses.
11+
public class CookieManager {
12+
13+
/// Shared instance of CookieManager.
14+
static let shared = CookieManager()
15+
16+
/// UserDefaults key for saving cookies.
17+
private static let userDefaultsKey = "SWIFTNETKIT_SAVED_COOKIES"
18+
19+
/// UserDefaults suite name for saving cookies.
20+
private let userDefaults = UserDefaults(suiteName: "SWIFTNETKIT_COOKIE_SUITE")
21+
22+
/// Flag indicating whether to sync cookies with UserDefaults.
23+
var syncCookiesWithUserDefaults: Bool = true
24+
25+
/// Private initializer to ensure singleton pattern.
26+
private init() {
27+
cleanExpiredCookies()
28+
syncCookies()
29+
}
30+
31+
/// Includes cookies in the given URLRequest if `includeCookies` is true.
32+
///
33+
/// - Parameters:
34+
/// - urlRequest: The URLRequest to include cookies in.
35+
/// - includeCookies: Boolean indicating whether to include cookies.
36+
func includeCookiesIfNeeded(for urlRequest: inout URLRequest, includeCookies: Bool) {
37+
if includeCookies {
38+
// Sync cookies with user defaults
39+
CookieManager.shared.syncCookies()
40+
41+
// Get cookies for the URL
42+
let cookies = CookieManager.shared.getCookiesForURL(for: urlRequest.url!)
43+
44+
// Create cookie header
45+
let cookieHeader = HTTPCookie.requestHeaderFields(with: cookies)
46+
47+
// Merge existing cookies with new cookies if any
48+
if let existingCookieHeader = urlRequest.allHTTPHeaderFields?[HTTPCookie.requestHeaderFields(with: []).keys.first ?? ""] {
49+
let mergedCookies = (existingCookieHeader + "; " + (cookieHeader.values.first ?? "")).trimmingCharacters(in: .whitespacesAndNewlines)
50+
urlRequest.allHTTPHeaderFields?[HTTPCookie.requestHeaderFields(with: []).keys.first ?? ""] = mergedCookies
51+
} else {
52+
urlRequest.allHTTPHeaderFields = urlRequest.allHTTPHeaderFields?.merging(cookieHeader) { (existing, new) in
53+
return existing + "; " + new
54+
} ?? cookieHeader
55+
}
56+
}
57+
}
58+
59+
/// Saves cookies from the response if `saveResponseCookies` is true.
60+
///
61+
/// - Parameters:
62+
/// - response: The URLResponse from which to save cookies.
63+
/// - saveResponseCookies: Boolean indicating whether to save response cookies.
64+
func saveCookiesIfNeeded(from response: URLResponse?, saveResponseCookies: Bool) {
65+
guard saveResponseCookies,
66+
let httpResponse = response as? HTTPURLResponse,
67+
let url = httpResponse.url else { return }
68+
69+
// Extract Set-Cookie headers
70+
let setCookieHeaders = httpResponse.allHeaderFields.filter { $0.key as? String == "Set-Cookie" }
71+
72+
// Create cookies from headers
73+
let cookies = HTTPCookie.cookies(withResponseHeaderFields: setCookieHeaders as! [String: String], for: url)
74+
75+
// Save cookies to session storage
76+
saveCookiesToSession(cookies)
77+
78+
// Save cookies to user defaults if syncCookiesWithUserDefaults is true
79+
if syncCookiesWithUserDefaults {
80+
saveCookiesToUserDefaults(cookies)
81+
}
82+
}
83+
84+
/// Retrieves cookies for a given URL.
85+
///
86+
/// - Parameter url: The URL for which to retrieve cookies.
87+
/// - Returns: An array of HTTPCookie objects.
88+
func getCookiesForURL(for url: URL) -> [HTTPCookie] {
89+
return HTTPCookieStorage.shared.cookies(for: url) ?? []
90+
}
91+
92+
/// Synchronizes cookies between session storage and user defaults.
93+
func syncCookies() {
94+
if syncCookiesWithUserDefaults {
95+
// Sync cookies from user defaults to session storage
96+
loadCookiesFromUserDefaults()
97+
98+
// Sync cookies from session storage to user defaults
99+
saveCookiesToUserDefaults(getAllCookiesFromSession())
100+
}
101+
}
102+
103+
/// Retrieves all cookies from the session storage.
104+
///
105+
/// - Returns: An array of all HTTPCookie objects in the session storage.
106+
func getAllCookiesFromSession() -> [HTTPCookie] {
107+
return HTTPCookieStorage.shared.cookies ?? []
108+
}
109+
110+
/// Saves cookies to the session storage.
111+
///
112+
/// - Parameter cookies: An array of HTTPCookie objects to save.
113+
func saveCookiesToSession(_ cookies: [HTTPCookie]) {
114+
for cookie in cookies {
115+
HTTPCookieStorage.shared.setCookie(cookie)
116+
}
117+
}
118+
119+
/// Saves cookies to user defaults.
120+
///
121+
/// - Parameter cookies: An array of HTTPCookie objects to save.
122+
func saveCookiesToUserDefaults(_ cookies: [HTTPCookie]) {
123+
var cookieDataArray: [Data] = []
124+
for cookie in cookies {
125+
if let data = try? NSKeyedArchiver.archivedData(withRootObject: cookie.properties ?? [:], requiringSecureCoding: false) {
126+
cookieDataArray.append(data)
127+
}
128+
}
129+
userDefaults?.set(cookieDataArray, forKey: CookieManager.userDefaultsKey)
130+
}
131+
132+
/// Loads cookies from user defaults into the session storage.
133+
func loadCookiesFromUserDefaults() {
134+
guard let cookieDataArray = userDefaults?.array(forKey: CookieManager.userDefaultsKey) as? [Data] else { return }
135+
136+
let allowedClasses: [AnyClass] = [NSDictionary.self, NSString.self, NSDate.self, NSNumber.self, NSURL.self]
137+
138+
for cookieData in cookieDataArray {
139+
if let cookieProperties = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: allowedClasses, from: cookieData) as? [HTTPCookiePropertyKey: Any],
140+
let cookie = HTTPCookie(properties: cookieProperties) {
141+
HTTPCookieStorage.shared.setCookie(cookie)
142+
}
143+
}
144+
}
145+
146+
/// Deletes all cookies from the session storage and user defaults.
147+
func deleteAllCookies() {
148+
// Delete all cookies from session storage
149+
HTTPCookieStorage.shared.cookies?.forEach(HTTPCookieStorage.shared.deleteCookie)
150+
151+
// Remove cookies from user defaults if syncCookiesWithUserDefaults is true
152+
if syncCookiesWithUserDefaults {
153+
userDefaults?.removeObject(forKey: CookieManager.userDefaultsKey)
154+
}
155+
}
156+
157+
/// Deletes expired cookies from the session storage and user defaults.
158+
func deleteExpiredCookies() {
159+
guard let cookieDataArray = userDefaults?.array(forKey: CookieManager.userDefaultsKey) as? [Data] else { return }
160+
var validCookieDataArray: [Data] = []
161+
162+
let allowedClasses: [AnyClass] = [NSDictionary.self, NSString.self, NSDate.self, NSNumber.self, NSURL.self]
163+
164+
for cookieData in cookieDataArray {
165+
if let cookieProperties = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: allowedClasses, from: cookieData) as? [HTTPCookiePropertyKey: Any],
166+
let cookie = HTTPCookie(properties: cookieProperties),
167+
cookie.expiresDate ?? Date.distantFuture > Date() {
168+
validCookieDataArray.append(cookieData)
169+
}
170+
}
171+
172+
// Save valid cookies back to user defaults
173+
userDefaults?.set(validCookieDataArray, forKey: CookieManager.userDefaultsKey)
174+
}
175+
176+
/// Cleans expired cookies from the session storage and user defaults.
177+
func cleanExpiredCookies() {
178+
deleteExpiredCookies()
179+
if syncCookiesWithUserDefaults {
180+
loadCookiesFromUserDefaults()
181+
}
182+
}
183+
}

0 commit comments

Comments
 (0)