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/8] 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/8] 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/8] 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/8] 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/8] 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/8] 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 From 84792c44d328a3a0b5bda2e9c47f2396addbfed7 Mon Sep 17 00:00:00 2001 From: "Stephen T. Sagarino Jr" Date: Tue, 7 Oct 2025 09:33:39 +0800 Subject: [PATCH 7/8] docs: update `readme` example code --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 40ad984..df8d96a 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,6 @@ let url = URL(string: "http://your.api/post")! let response = await SNK .request(url: url) - .jsonContentType() .body(user) .post(validateBodyAs: UserResponse.self) @@ -79,7 +78,6 @@ let customSNK = SNKSession( let response = try await customSNK .request(path: "/post") - .jsonContentType() .body(user) .post(validateBodyAs: UserResponse.self) From 67b8ea14bdccc425bf7255ced015299cdc640a43 Mon Sep 17 00:00:00 2001 From: "Stephen T. Sagarino Jr" Date: Tue, 7 Oct 2025 09:39:08 +0800 Subject: [PATCH 8/8] ci: add `merge` conventional commit --- .github/scripts/commitlint-config-maker.sh | 3 ++- .github/workflows/pr-commitlint.yml | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/scripts/commitlint-config-maker.sh b/.github/scripts/commitlint-config-maker.sh index a86e22e..c0855ed 100644 --- a/.github/scripts/commitlint-config-maker.sh +++ b/.github/scripts/commitlint-config-maker.sh @@ -15,7 +15,7 @@ echo ' "subject-empty": [RuleConfigSeverity.Error, "never"] as const,' >> com echo ' "subject-full-stop": [RuleConfigSeverity.Error, "never", "."] as const,' >> commitlint.config.ts echo ' "type-case": [RuleConfigSeverity.Error, "always", "lower-case"] as const,' >> commitlint.config.ts echo ' "type-empty": [RuleConfigSeverity.Error, "never"] as const,' >> commitlint.config.ts -echo ' "type-enum": [RuleConfigSeverity.Error, "always", ["build", "chore", "ci", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "test"]] as [RuleConfigSeverity, RuleConfigCondition, string[]],' >> commitlint.config.ts +echo ' "type-enum": [RuleConfigSeverity.Error, "always", ["build", "chore", "ci", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "test", "merge"]] as [RuleConfigSeverity, RuleConfigCondition, string[]],' >> commitlint.config.ts echo ' },' >> commitlint.config.ts echo ' prompt: {' >> commitlint.config.ts echo ' questions: {' >> commitlint.config.ts @@ -32,6 +32,7 @@ echo ' test: { description: "Adding missing tests or correcting existin echo ' build: { description: "Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)", title: "Builds", emoji: "🛠" },' >> commitlint.config.ts echo ' ci: { description: "Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)", title: "Continuous Integrations", emoji: "⚙️" },' >> commitlint.config.ts echo ' chore: { description: "Other changes that don'\''t modify src or test files", title: "Chores", emoji: "♻️" },' >> commitlint.config.ts +echo ' build: { description: "Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)", title: "Builds", emoji: "🛠" },' >> commitlint.config.ts echo ' revert: { description: "Reverts a previous commit", title: "Reverts", emoji: "🗑" },' >> commitlint.config.ts echo ' },' >> commitlint.config.ts echo ' },' >> commitlint.config.ts diff --git a/.github/workflows/pr-commitlint.yml b/.github/workflows/pr-commitlint.yml index 3047a3f..be260f8 100644 --- a/.github/workflows/pr-commitlint.yml +++ b/.github/workflows/pr-commitlint.yml @@ -1,6 +1,10 @@ name: Pull Request Commit Lint -on: pull_request +on: + pull_request: + branches: + - main + - develop jobs: commitlint: runs-on: ubuntu-latest