From 2bdbc7519ff9ee6f6a387b0869688d189be29a40 Mon Sep 17 00:00:00 2001 From: "Stephen T. Sagarino Jr" Date: Tue, 7 Oct 2025 08:29:40 +0800 Subject: [PATCH 1/6] chore: create root `ContentType` enum --- .../SwiftNetworkKit/Core/ContentType.swift | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 Sources/SwiftNetworkKit/Core/ContentType.swift diff --git a/Sources/SwiftNetworkKit/Core/ContentType.swift b/Sources/SwiftNetworkKit/Core/ContentType.swift new file mode 100644 index 0000000..35e32ec --- /dev/null +++ b/Sources/SwiftNetworkKit/Core/ContentType.swift @@ -0,0 +1,22 @@ +// +// ContentType.swift +// SwiftNetworkKit +// +// Created by Stephen T. Sagarino Jr. on 10/7/25. +// + +import Foundation + +public enum ContentType: String { + case json = "application/json" + case formURLEncoded = "application/x-www-form-urlencoded" + case multipartFormData = "multipart/form-data" + case textPlain = "text/plain" + case textHTML = "text/html" + case applicationXML = "application/xml" + case textXML = "text/xml" + case applicationPDF = "application/pdf" + case imagePNG = "image/png" + case imageJPEG = "image/jpeg" + case applicationOctetStream = "application/octet-stream" +} From 9f02c1d29c0b3f9cf4bc31651b3c02b3d875fe98 Mon Sep 17 00:00:00 2001 From: "Stephen T. Sagarino Jr" Date: Tue, 7 Oct 2025 08:30:22 +0800 Subject: [PATCH 2/6] chore: add `contentType` variable for SNKDataRequest created setter and variable type for it --- .../SwiftNetworkKit/Core/SNKDataRequest.swift | 50 +++++++++++++++---- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/Sources/SwiftNetworkKit/Core/SNKDataRequest.swift b/Sources/SwiftNetworkKit/Core/SNKDataRequest.swift index e4d8266..4d67670 100644 --- a/Sources/SwiftNetworkKit/Core/SNKDataRequest.swift +++ b/Sources/SwiftNetworkKit/Core/SNKDataRequest.swift @@ -5,7 +5,6 @@ // Created by Stephen T. Sagarino Jr. on 10/2/25. // -import Combine import Foundation open class SNKDataRequest: @unchecked Sendable { @@ -15,25 +14,29 @@ open class SNKDataRequest: @unchecked Sendable { /// the URLRequest used for the request var urlRequest: URLRequest /// the headers of the request - /// default is nil - /// use the `headers(_:)` function to set + /// - default is `nil` + /// - use the `headers(_:)` function to set var headers: [String: String]? /// the parameters of the request - /// default is nil - /// use the `queryParams(_:)` function to set + /// - default is `nil` + /// - use the `queryParams(_:)` function to set var queryParams: [String: String]? /// the body of the request - /// default is nil - /// use the `body(_:)` function to set + /// - default is `nil` + /// - use the `body(_:)` function to set var body: Encodable? /// the decoder used to decode the response - /// default is JSONDecoder() - /// use the `decoder(_:)` function to set + /// - default is `JSONDecoder()` + /// - use the `decoder(_:)` function to set var decoder: JSONDecoder = JSONDecoder() /// the encoder used to encode the request body - /// default is JSONEncoder() - /// use the `encoder(_:)` function to set + /// - Default: `JSONEncoder()` + /// - use the `encoder(_:)` function to set var encoder: JSONEncoder = JSONEncoder() + /// The Content-Type of the request body. + /// - Default: `.json` is used by default. + /// - Set using the `contentType(_:)` method. + var contentType: SwiftNetworkKit.ContentType = .json init( _ url: URL, @@ -42,6 +45,28 @@ open class SNKDataRequest: @unchecked Sendable { self.urlRequest = URLRequest(url: url) self.urlSession = urlSession } + + /// Sets the Content-Type for the request body. + /// + /// Use this method to specify the MIME type of the request body, such as `.json` or `.formURLEncoded`. + /// The Content-Type header informs the server about the format of the data being sent. + /// + /// - Parameter contentType: The desired `ContentType` for the request body. + /// - Returns: The same `SNKDataRequest` instance to allow method chaining. + /// + /// ## Example + /// ```swift + /// let request = SNKDataRequest(url) + /// .contentType(.json) + /// .post() + /// ``` + public func contentType( + _ contentType: SwiftNetworkKit.ContentType + ) -> SNKDataRequest { + self.contentType = contentType + return self + } + /// Executes an HTTP request and returns the raw response data without decoding. /// /// This internal method performs the actual HTTP request using URLSession and returns @@ -825,6 +850,7 @@ extension SNKDataRequest { /// - `queryParams`: Dictionary converted to URL query items /// - `headers`: Applied as HTTP header fields /// - `body`: Set as the HTTP request body + /// - `contentType`: Set as the HTTP request body's Content-Type header, defaults to `application/json` /// /// - Important: This method should only be called after all request configuration is complete. fileprivate func request(_ method: HTTPMethod) throws -> URLRequest { @@ -840,6 +866,8 @@ extension SNKDataRequest { request.httpMethod = method.rawValue request.allHTTPHeaderFields = self.headers + self.addHeader("Content-Type", value: self.contentType.rawValue) + if let body = self.body { let body = try self.encoder.encode(body) request.httpBody = body From dc4622ef2e127aa3c2aa849c90662105f8ac5c8a Mon Sep 17 00:00:00 2001 From: "Stephen T. Sagarino Jr" Date: Tue, 7 Oct 2025 08:30:43 +0800 Subject: [PATCH 3/6] refactor: deprecates all the `SNKDataRequest.ContentType` --- .../Extensions/SNKDataRequestExtension.swift | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/Sources/SwiftNetworkKit/Extensions/SNKDataRequestExtension.swift b/Sources/SwiftNetworkKit/Extensions/SNKDataRequestExtension.swift index 1aed5c0..6a9dea7 100644 --- a/Sources/SwiftNetworkKit/Extensions/SNKDataRequestExtension.swift +++ b/Sources/SwiftNetworkKit/Extensions/SNKDataRequestExtension.swift @@ -6,6 +6,11 @@ // extension SNKDataRequest { + @available( + *, + deprecated, + message: "Use SwiftNetworkKit.ContentType instead" + ) internal struct ContentType { public static let json = "application/json" public static let formURLEncoded = "application/x-www-form-urlencoded" @@ -20,16 +25,31 @@ extension SNKDataRequest { public static let applicationOctetStream = "application/octet-stream" } + @available( + *, + deprecated, + message: "Use .contentType(.json) instead" + ) /// Sets the Content-Type header to application/json public func jsonContentType() -> SNKDataRequest { return self.addHeader("Content-Type", value: ContentType.json) } + @available( + *, + deprecated, + message: "Use .contentType(.formURLEncoded) instead" + ) /// Sets the Content-Type header to application/x-www-form-urlencoded public func formContentType() -> SNKDataRequest { return self.addHeader("Content-Type", value: ContentType.formURLEncoded) } + @available( + *, + deprecated, + message: "Use .contentType(.multipartFormData) instead" + ) /// Sets the Content-Type header to multipart/form-data public func multipartContentType() -> SNKDataRequest { return self.addHeader( @@ -38,36 +58,71 @@ extension SNKDataRequest { ) } + @available( + *, + deprecated, + message: "Use .contentType(.textPlain) instead" + ) /// Sets the Content-Type header to text/plain public func textContentType() -> SNKDataRequest { return self.addHeader("Content-Type", value: ContentType.textPlain) } + @available( + *, + deprecated, + message: "Use .contentType(.textHTML) instead" + ) /// Sets the Content-Type header to text/html public func htmlContentType() -> SNKDataRequest { return self.addHeader("Content-Type", value: ContentType.textHTML) } + @available( + *, + deprecated, + message: "Use .contentType(.applicationXML) instead" + ) /// Sets the Content-Type header to application/xml public func xmlContentType() -> SNKDataRequest { return self.addHeader("Content-Type", value: ContentType.applicationXML) } + @available( + *, + deprecated, + message: "Use .contentType(.applicationPDF) instead" + ) /// Sets the Content-Type header to application/pdf public func pdfContentType() -> SNKDataRequest { return self.addHeader("Content-Type", value: ContentType.applicationPDF) } + @available( + *, + deprecated, + message: "Use .contentType(.imagePNG) instead" + ) /// Sets the Content-Type header to image/png public func pngContentType() -> SNKDataRequest { return self.addHeader("Content-Type", value: ContentType.imagePNG) } + @available( + *, + deprecated, + message: "Use .contentType(.imageJPEG) instead" + ) /// Sets the Content-Type header to image/jpeg public func jpegContentType() -> SNKDataRequest { return self.addHeader("Content-Type", value: ContentType.imageJPEG) } + @available( + *, + deprecated, + message: "Use .contentType(.applicationOctetStream) instead" + ) /// Sets the Content-Type header to application/octet-stream public func binaryContentType() -> SNKDataRequest { return self.addHeader( From aaf03d7d78c82ef2ce5e9c6a1e0b5f5bd9e35ce3 Mon Sep 17 00:00:00 2001 From: "Stephen T. Sagarino Jr" Date: Tue, 7 Oct 2025 08:33:45 +0800 Subject: [PATCH 4/6] refactor(example): edit `.jsonContentType()` to `.contentType(.json)` --- Example/Example/ContentView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Example/Example/ContentView.swift b/Example/Example/ContentView.swift index 0f3ae52..ca041d4 100644 --- a/Example/Example/ContentView.swift +++ b/Example/Example/ContentView.swift @@ -90,7 +90,7 @@ struct ContentView: View { let response = await SNK .request(url: URL(string: "https://httpbin.org/post")!) - .jsonContentType() + .contentType(.json) .body(testData) .post(validateBodyAs: HttpBinResponse.self) @@ -120,7 +120,7 @@ struct ContentView: View { let response = await SNK .request(url: URL(string: "https://httpbin.org/post")!) - .formContentType() + .contentType(.json) .body(formData) .post(validateBodyAs: HttpBinResponse.self) @@ -219,7 +219,7 @@ struct ContentView: View { let response = await SNK .request(url: URL(string: "https://httpbin.org/post")!) - .xmlContentType() + .contentType(.json) .post(validateBodyAs: HttpBinResponse.self) let result = TestResult( From bc10436b283c39ec22c5c463c77c699fa07415c4 Mon Sep 17 00:00:00 2001 From: "Stephen T. Sagarino Jr" Date: Tue, 7 Oct 2025 09:09:01 +0800 Subject: [PATCH 5/6] feat(image-upload): can now set `Data` as body of http request --- .../SwiftNetworkKit/Core/SNKDataRequest.swift | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftNetworkKit/Core/SNKDataRequest.swift b/Sources/SwiftNetworkKit/Core/SNKDataRequest.swift index 4d67670..e2656ec 100644 --- a/Sources/SwiftNetworkKit/Core/SNKDataRequest.swift +++ b/Sources/SwiftNetworkKit/Core/SNKDataRequest.swift @@ -183,7 +183,7 @@ open class SNKDataRequest: @unchecked Sendable { return SNKResponse( data: nil, status: validatedOutput.status, - error: nil + error: validatedOutput.status?.asError() ) } @@ -752,6 +752,29 @@ extension SNKDataRequest { self.body = body return self } + + /// Sets the request body using raw `Data`. + /// + /// Use this method to provide a pre-encoded or binary payload as the HTTP request body. + /// This is useful for sending files, images, or custom-encoded data formats. + /// + /// - Parameter body: The raw `Data` to include in the request body. + /// - Returns: The same `SNKDataRequest` instance to enable method chaining. + /// + /// ## Example + /// ```swift + /// let imageData: Data = ... // Load image data + /// let request = SNKDataRequest(url) + /// .contentType(.imagePNG) + /// .body(imageData) + /// .post() + /// ``` + /// + /// - Important: Ensure the `Content-Type` header matches the format of your body data. + public func body(_ body: Data) -> SNKDataRequest { + self.body = body + return self + } } // MARK: - Helper Functions From 3bdda418f4bbaa4bb5f2e9ac6cbdc9ad7948c091 Mon Sep 17 00:00:00 2001 From: "Stephen T. Sagarino Jr" Date: Tue, 7 Oct 2025 09:23:53 +0800 Subject: [PATCH 6/6] feat(cache): add `cachePolicy` setter for SNKDataRequest --- .../SwiftNetworkKit/Core/SNKDataRequest.swift | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/Sources/SwiftNetworkKit/Core/SNKDataRequest.swift b/Sources/SwiftNetworkKit/Core/SNKDataRequest.swift index e2656ec..c4551af 100644 --- a/Sources/SwiftNetworkKit/Core/SNKDataRequest.swift +++ b/Sources/SwiftNetworkKit/Core/SNKDataRequest.swift @@ -37,6 +37,14 @@ open class SNKDataRequest: @unchecked Sendable { /// - Default: `.json` is used by default. /// - Set using the `contentType(_:)` method. var contentType: SwiftNetworkKit.ContentType = .json + /// The timeout interval for the request. + /// - Default: `nil`, which uses the URLSession's default timeout. + /// - Set using the `timeoutInterval(_:)` method. + var timeoutInterval: TimeInterval? + /// The cache policy for the request. + /// - Default: `nil`, which uses the URLSession's default cache policy. + /// - Set using the `cachePolicy(_:)` method. + var cachePolicy: URLRequest.CachePolicy? init( _ url: URL, @@ -46,6 +54,51 @@ open class SNKDataRequest: @unchecked Sendable { self.urlSession = urlSession } + /// Sets the cache policy for the request. + /// + /// Use this method to specify how the request should interact with the local cache. + /// The cache policy determines whether the request should use cached data, ignore the cache, + /// or fall back to the cache if the network is unavailable. + /// + /// - Parameter cachePolicy: The `URLRequest.CachePolicy` to use for this request. + /// - Returns: The same `SNKDataRequest` instance to enable method chaining. + /// + /// ## Example + /// ```swift + /// let request = SNKDataRequest(url) + /// .cachePolicy(.reloadIgnoringLocalCacheData) + /// .get() + /// ``` + /// + /// - Note: If not set, the default cache policy of the underlying `URLSession` is used. + public func cachePolicy( + _ cachePolicy: URLRequest.CachePolicy + ) -> SNKDataRequest { + self.cachePolicy = cachePolicy + return self + } + + /// Sets the timeout interval for the request. + /// + /// Use this method to specify how long (in seconds) the request should wait before timing out. + /// If not set, the default timeout interval of the underlying `URLSession` is used. + /// + /// - Parameter timeoutInterval: The timeout interval, in seconds. + /// - Returns: The same `SNKDataRequest` instance to enable method chaining. + /// + /// ## Example + /// ```swift + /// let request = SNKDataRequest(url) + /// .timeoutInterval(30) + /// .get() + /// ``` + public func timeoutInterval( + _ timeoutInterval: TimeInterval + ) -> SNKDataRequest { + self.timeoutInterval = timeoutInterval + return self + } + /// Sets the Content-Type for the request body. /// /// Use this method to specify the MIME type of the request body, such as `.json` or `.formURLEncoded`. @@ -854,6 +907,7 @@ extension SNKDataRequest { /// 2. Appends query parameters to the URL if any are configured /// 3. Sets the HTTP method from the provided parameter /// 4. Applies all configured headers to the request + /// - Automatically adds the `Content-Type` header based on `contentType`, default is `application/json` /// 5. Attaches the request body data if present /// /// ## Query Parameter Handling @@ -891,6 +945,14 @@ extension SNKDataRequest { self.addHeader("Content-Type", value: self.contentType.rawValue) + if let timeoutInterval = self.timeoutInterval { + request.timeoutInterval = timeoutInterval + } + + if let cachePolicy = self.cachePolicy { + request.cachePolicy = cachePolicy + } + if let body = self.body { let body = try self.encoder.encode(body) request.httpBody = body