mirror of
https://github.com/samsonjs/Osiris.git
synced 2026-03-25 08:55:48 +00:00
Add support for query params on GET and DELETE requests
This commit is contained in:
parent
863d712e42
commit
77446ccf2d
5 changed files with 122 additions and 22 deletions
|
|
@ -1,8 +1,9 @@
|
|||
# Changelog
|
||||
|
||||
## [2.0.0] - 2025-06-15
|
||||
## [2.0.0] - Unreleased
|
||||
|
||||
### Added
|
||||
- **GET/DELETE query parameter support** - Parameters are now automatically encoded as query strings for GET and DELETE requests
|
||||
- **Enhanced error types** with localized descriptions and failure reasons
|
||||
- **Header convenience method** `addHeader(name:value:)` on `HTTPRequest`
|
||||
- **Comprehensive test coverage**
|
||||
|
|
|
|||
12
Readme.md
12
Readme.md
|
|
@ -76,6 +76,14 @@ Basic usage:
|
|||
|
||||
```swift
|
||||
let url = URL(string: "https://example.net")!
|
||||
|
||||
// GET request with query parameters
|
||||
let getRequest = HTTPRequest.get(url, parameters: ["page": "1", "limit": "10"])
|
||||
|
||||
// DELETE request with query parameters
|
||||
let deleteRequest = HTTPRequest.delete(url, parameters: ["confirm": "true"])
|
||||
|
||||
// Or use the general initializer
|
||||
let request = HTTPRequest(method: .get, url: url)
|
||||
```
|
||||
|
||||
|
|
@ -84,7 +92,9 @@ More advanced usage with parameters and headers:
|
|||
```swift
|
||||
let url = URL(string: "https://example.net")!
|
||||
let params = ["email": "freddie@example.net", "password": "BohemianRhapsody"]
|
||||
let request = HTTPRequest(method: .post, url: url, contentType: .json, parameters: params)
|
||||
|
||||
// POST with JSON parameters (goes in request body)
|
||||
let request = HTTPRequest.post(url, contentType: .json, parameters: params)
|
||||
request.addHeader(name: "x-custom", value: "42")
|
||||
request.addMultipartJPEG(name: "avatar", image: UIImage(), quality: 1, filename: "avatar.jpg")
|
||||
```
|
||||
|
|
|
|||
|
|
@ -21,8 +21,11 @@ private let log = Logger(subsystem: "co.1se.Osiris", category: "HTTPRequest")
|
|||
/// ## Usage
|
||||
///
|
||||
/// ```swift
|
||||
/// // Simple GET request
|
||||
/// let request = HTTPRequest.get(URL(string: "https://api.example.net/users")!)
|
||||
/// // GET request with query parameters
|
||||
/// let getRequest = HTTPRequest.get(
|
||||
/// URL(string: "https://api.example.net/users")!,
|
||||
/// parameters: ["page": "1", "limit": "10"]
|
||||
/// )
|
||||
///
|
||||
/// // POST with JSON parameters
|
||||
/// let jsonRequest = HTTPRequest.post(
|
||||
|
|
@ -31,6 +34,12 @@ private let log = Logger(subsystem: "co.1se.Osiris", category: "HTTPRequest")
|
|||
/// parameters: ["name": "Jane", "email": "jane@example.net"]
|
||||
/// )
|
||||
///
|
||||
/// // DELETE with query parameters
|
||||
/// let deleteRequest = HTTPRequest.delete(
|
||||
/// URL(string: "https://api.example.net/users/123")!,
|
||||
/// parameters: ["confirm": "true"]
|
||||
/// )
|
||||
///
|
||||
/// // Multipart form with file upload
|
||||
/// var multipartRequest = HTTPRequest.post(URL(string: "https://api.example.net/upload")!)
|
||||
/// multipartRequest.parts = [
|
||||
|
|
@ -78,10 +87,10 @@ public struct HTTPRequest: Sendable, CustomStringConvertible {
|
|||
/// Creates a GET request.
|
||||
/// - Parameters:
|
||||
/// - url: The target URL
|
||||
/// - contentType: The content type (typically .none for GET)
|
||||
/// - parameters: Optional parameters to include as query string
|
||||
/// - Returns: A configured HTTPRequest
|
||||
public static func get(_ url: URL, contentType: HTTPContentType = .none) -> HTTPRequest {
|
||||
HTTPRequest(method: .get, url: url, contentType: contentType)
|
||||
public static func get(_ url: URL, parameters: [String: any Sendable]? = nil) -> HTTPRequest {
|
||||
HTTPRequest(method: .get, url: url, contentType: .none, parameters: parameters)
|
||||
}
|
||||
|
||||
/// Creates a PUT request.
|
||||
|
|
@ -107,10 +116,10 @@ public struct HTTPRequest: Sendable, CustomStringConvertible {
|
|||
/// Creates a DELETE request.
|
||||
/// - Parameters:
|
||||
/// - url: The target URL
|
||||
/// - contentType: The content type (typically .none for DELETE)
|
||||
/// - parameters: Optional parameters to include as query string
|
||||
/// - Returns: A configured HTTPRequest
|
||||
public static func delete(_ url: URL, contentType: HTTPContentType = .none) -> HTTPRequest {
|
||||
HTTPRequest(method: .delete, url: url, contentType: contentType)
|
||||
public static func delete(_ url: URL, parameters: [String: any Sendable]? = nil) -> HTTPRequest {
|
||||
HTTPRequest(method: .delete, url: url, contentType: .none, parameters: parameters)
|
||||
}
|
||||
|
||||
#if canImport(UIKit)
|
||||
|
|
|
|||
|
|
@ -42,24 +42,22 @@ public final class RequestBuilder {
|
|||
|
||||
/// Converts an HTTPRequest to a URLRequest ready for use with URLSession.
|
||||
///
|
||||
/// This method handles encoding of parameters according to the request's content type:
|
||||
/// - `.json`: Parameters are encoded as JSON in the request body
|
||||
/// - `.formEncoded`: Parameters are URL-encoded in the request body
|
||||
/// This method handles encoding of parameters according to the request's method and content type:
|
||||
/// - **GET/DELETE**: Parameters are encoded as query string parameters
|
||||
/// - `.json`: Parameters are encoded as JSON in the request body (POST/PUT/PATCH)
|
||||
/// - `.formEncoded`: Parameters are URL-encoded in the request body (POST/PUT/PATCH)
|
||||
/// - `.multipart`: Parts are encoded as multipart/form-data (in memory)
|
||||
/// - `.none`: Falls back to form encoding for compatibility
|
||||
///
|
||||
/// - Parameter request: The HTTPRequest to convert
|
||||
/// - Returns: A URLRequest ready for URLSession
|
||||
/// - Throws: `RequestBuilderError.invalidFormData` if form encoding fails,
|
||||
/// or various encoding errors from JSONSerialization or MultipartFormEncoder
|
||||
/// - Throws: `RequestBuilderError.invalidFormData` if form encoding fails, if GET/DELETE
|
||||
/// requests contain multipart parts, or various encoding errors from JSONSerialization
|
||||
/// or MultipartFormEncoder
|
||||
///
|
||||
/// - Note: GET and DELETE requests with parameters are not currently supported
|
||||
/// - Warning: Multipart requests are encoded entirely in memory. For large files,
|
||||
/// consider using MultipartFormEncoder.encodeFile() directly
|
||||
public class func build(request: HTTPRequest) throws -> URLRequest {
|
||||
assert(!(request.method == .get && request.parameters != nil), "encoding GET params is not yet implemented")
|
||||
assert(!(request.method == .delete && request.parameters != nil), "encoding DELETE params is not yet implemented")
|
||||
|
||||
var result = URLRequest(url: request.url)
|
||||
result.httpMethod = request.method.string
|
||||
|
||||
|
|
@ -67,8 +65,14 @@ public final class RequestBuilder {
|
|||
result.addValue(value, forHTTPHeaderField: name)
|
||||
}
|
||||
|
||||
// When parts are provided then override to be multipart regardless of the content type.
|
||||
if !request.parts.isEmpty || request.contentType == .multipart {
|
||||
// Handle parameters based on HTTP method
|
||||
if request.method == .get || request.method == .delete, let params = request.parameters {
|
||||
// Validate that GET and DELETE requests don't want request bodies, which we don't support.
|
||||
guard request.contentType != .multipart, request.parts.isEmpty else {
|
||||
throw RequestBuilderError.invalidFormData(request)
|
||||
}
|
||||
try encodeQueryParameters(to: &result, parameters: params)
|
||||
} else if !request.parts.isEmpty || request.contentType == .multipart {
|
||||
if request.contentType != .multipart {
|
||||
log.info("Encoding request as multipart, overriding its content type of \(request.contentType)")
|
||||
}
|
||||
|
|
@ -76,7 +80,7 @@ public final class RequestBuilder {
|
|||
} else if let params = request.parameters {
|
||||
try encodeParameters(to: &result, request: request, parameters: params)
|
||||
}
|
||||
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
|
@ -116,4 +120,23 @@ public final class RequestBuilder {
|
|||
}
|
||||
urlRequest.httpBody = formData
|
||||
}
|
||||
|
||||
private class func encodeQueryParameters(to urlRequest: inout URLRequest, parameters: [String: any Sendable]) throws {
|
||||
guard let url = urlRequest.url else {
|
||||
return
|
||||
}
|
||||
|
||||
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
|
||||
let newQueryItems = parameters.compactMap { (key, value) -> URLQueryItem? in
|
||||
URLQueryItem(name: key, value: String(describing: value))
|
||||
}
|
||||
|
||||
if let existingQueryItems = components?.queryItems {
|
||||
components?.queryItems = existingQueryItems + newQueryItems
|
||||
} else {
|
||||
components?.queryItems = newQueryItems
|
||||
}
|
||||
|
||||
urlRequest.url = components?.url ?? url
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -236,4 +236,61 @@ class RequestBuilderTests: XCTestCase {
|
|||
XCTAssertTrue(bodyString.contains("band=Queen"))
|
||||
}
|
||||
|
||||
func testBuildGETRequestWithQueryParameters() throws {
|
||||
let httpRequest = HTTPRequest.get(baseURL, parameters: ["name": "John Doe", "email": "john@example.net"])
|
||||
let urlRequest = try RequestBuilder.build(request: httpRequest)
|
||||
|
||||
XCTAssertEqual(urlRequest.httpMethod, "GET")
|
||||
XCTAssertNil(urlRequest.httpBody)
|
||||
XCTAssertNil(urlRequest.value(forHTTPHeaderField: "Content-Type"))
|
||||
|
||||
let urlString = urlRequest.url?.absoluteString ?? ""
|
||||
XCTAssertTrue(urlString.contains("name=John%20Doe"), "URL should contain encoded name parameter")
|
||||
XCTAssertTrue(urlString.contains("email=john@example.net"), "URL should contain email parameter")
|
||||
XCTAssertTrue(urlString.contains("?"), "URL should contain query separator")
|
||||
}
|
||||
|
||||
func testBuildDELETERequestWithQueryParameters() throws {
|
||||
let httpRequest = HTTPRequest.delete(baseURL, parameters: ["id": "123", "confirm": "true"])
|
||||
let urlRequest = try RequestBuilder.build(request: httpRequest)
|
||||
|
||||
XCTAssertEqual(urlRequest.httpMethod, "DELETE")
|
||||
XCTAssertNil(urlRequest.httpBody)
|
||||
XCTAssertNil(urlRequest.value(forHTTPHeaderField: "Content-Type"))
|
||||
|
||||
let urlString = urlRequest.url?.absoluteString ?? ""
|
||||
XCTAssertTrue(urlString.contains("id=123"))
|
||||
XCTAssertTrue(urlString.contains("confirm=true"))
|
||||
XCTAssertTrue(urlString.contains("?"))
|
||||
}
|
||||
|
||||
func testBuildGETRequestWithExistingQueryString() throws {
|
||||
let urlWithQuery = URL(string: "https://api.example.net/users?existing=param")!
|
||||
let httpRequest = HTTPRequest.get(urlWithQuery, parameters: ["new": "value"])
|
||||
let urlRequest = try RequestBuilder.build(request: httpRequest)
|
||||
|
||||
let urlString = urlRequest.url?.absoluteString ?? ""
|
||||
XCTAssertTrue(urlString.contains("existing=param"))
|
||||
XCTAssertTrue(urlString.contains("new=value"))
|
||||
XCTAssertTrue(urlString.contains("&"))
|
||||
}
|
||||
|
||||
func testBuildGETRequestWithMultipartThrowsError() throws {
|
||||
var httpRequest = HTTPRequest.get(baseURL, parameters: ["name": "value"])
|
||||
httpRequest.contentType = HTTPContentType.multipart
|
||||
|
||||
XCTAssertThrowsError(try RequestBuilder.build(request: httpRequest)) { error in
|
||||
XCTAssertTrue(error is RequestBuilderError)
|
||||
}
|
||||
}
|
||||
|
||||
func testBuildDELETERequestWithPartsThrowsError() throws {
|
||||
var httpRequest = HTTPRequest.delete(baseURL, parameters: ["id": "123"])
|
||||
httpRequest.parts = [MultipartFormEncoder.Part.text("value", name: "test")]
|
||||
|
||||
XCTAssertThrowsError(try RequestBuilder.build(request: httpRequest)) { error in
|
||||
XCTAssertTrue(error is RequestBuilderError)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue