Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Example/Example/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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(
Expand Down
22 changes: 22 additions & 0 deletions Sources/SwiftNetworkKit/Core/ContentType.swift
Original file line number Diff line number Diff line change
@@ -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"
}
137 changes: 125 additions & 12 deletions Sources/SwiftNetworkKit/Core/SNKDataRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
// Created by Stephen T. Sagarino Jr. on 10/2/25.
//

import Combine
import Foundation

open class SNKDataRequest: @unchecked Sendable {
Expand All @@ -15,25 +14,37 @@ 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
/// 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,
Expand All @@ -42,6 +53,73 @@ open class SNKDataRequest: @unchecked Sendable {
self.urlRequest = URLRequest(url: url)
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`.
/// 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
Expand Down Expand Up @@ -158,7 +236,7 @@ open class SNKDataRequest: @unchecked Sendable {
return SNKResponse(
data: nil,
status: validatedOutput.status,
error: nil
error: validatedOutput.status?.asError()
)
}

Expand Down Expand Up @@ -727,6 +805,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
Expand Down Expand Up @@ -806,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
Expand All @@ -825,6 +927,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 {
Expand All @@ -840,6 +943,16 @@ extension SNKDataRequest {
request.httpMethod = method.rawValue
request.allHTTPHeaderFields = self.headers

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
Expand Down
55 changes: 55 additions & 0 deletions Sources/SwiftNetworkKit/Extensions/SNKDataRequestExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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(
Expand All @@ -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(
Expand Down
Loading