mirror of
https://github.com/samsonjs/Osiris.git
synced 2026-03-25 08:55:48 +00:00
This introduces a cleaner, more intuitive API for making HTTP requests
with explicit methods for different content types and built-in Codable
support.
**New**
- Add explicit request methods: .postJSON(), .postForm(),
.postMultipart() for clear intent
- Add direct `Codable` body support with automatic JSON
encoding/decoding
- Add `HTTPRequestBody` enum for internal type safety and cleaner
implementation
- Add proper query parameter encoding for GET and DELETE requests
(previously ignored)
- Add URLSession extensions for streamlined async JSON decoding with
`HTTPError` for failure response status codes
- Add comprehensive test coverage
The new API replaces the parameter-based methods using dictionaries with
explicitly-typed ones. Instead of passing a content-type parameter, you
now use purpose-built methods like `postJSON` and `postForm`.
**Breaking changes**
- Minimum deployment targets raised to iOS 16.0 and macOS 13.0
- Direct access to `parameters` and `parts` properties deprecated on
`HTTPRequest`
- GET and DELETE requests now validate that they don't have request
bodies, and the new API prevents you from constructing them
309 lines
10 KiB
Swift
309 lines
10 KiB
Swift
//
|
|
// Created by Sami Samhuri on 2025-06-23.
|
|
// Copyright © 2025 Sami Samhuri. All rights reserved.
|
|
// Released under the terms of the MIT license.
|
|
//
|
|
|
|
import Foundation
|
|
@testable import Osiris
|
|
|
|
func httpRequestWithCodableSupport() async throws {
|
|
let url = URL(string: "https://trails.example.net/riders")!
|
|
|
|
// GET request with automatic JSON decoding
|
|
let riders: [RiderProfile] = try await URLSession.shared.perform(.get(url))
|
|
|
|
// POST with Codable body and automatic response decoding
|
|
struct CreateRiderRequest: Codable {
|
|
let name: String
|
|
let email: String
|
|
let bike: String
|
|
}
|
|
|
|
let danny = CreateRiderRequest(name: "Danny MacAskill", email: "danny@trails.example.net", bike: "Santa Cruz 5010")
|
|
let created: RiderProfile = try await URLSession.shared.perform(.post(url, body: danny))
|
|
|
|
// Custom encoder/decoder
|
|
let encoder = JSONEncoder()
|
|
encoder.keyEncodingStrategy = .convertToSnakeCase
|
|
let customRequest = try HTTPRequest.post(url, body: danny, encoder: encoder)
|
|
|
|
let decoder = JSONDecoder()
|
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
|
let createdAgain: RiderProfile = try await URLSession.shared.perform(customRequest, decoder: decoder)
|
|
|
|
// For requests expecting no content (204, etc.)
|
|
try await URLSession.shared.perform(.delete(url.appendingPathComponent("123")))
|
|
|
|
_ = riders
|
|
_ = created
|
|
_ = createdAgain
|
|
}
|
|
|
|
func basicHTTPRequest() throws {
|
|
let url = URL(string: "https://example.net/kittens")!
|
|
|
|
// Basic GET request
|
|
let request = HTTPRequest.get(url)
|
|
|
|
// 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"])
|
|
|
|
_ = request
|
|
_ = getRequest
|
|
_ = deleteRequest
|
|
}
|
|
|
|
func moreComplicatedPOSTRequest() throws {
|
|
let url = URL(string: "https://example.net/band")!
|
|
let params = ["email": "fatmike@example.net", "password": "LinoleumSupportsMyHead"]
|
|
|
|
// POST with JSON body
|
|
let jsonRequest = HTTPRequest.postJSON(url, body: params)
|
|
|
|
// POST with form-encoded body
|
|
let formRequest = HTTPRequest.postForm(url, parameters: params)
|
|
|
|
// POST with multipart body
|
|
let multipartRequest = HTTPRequest.postMultipart(url, parts: [.text("all day", name: "album")])
|
|
|
|
_ = jsonRequest
|
|
_ = formRequest
|
|
_ = multipartRequest
|
|
}
|
|
|
|
func requestBuilderExample() throws {
|
|
let url = URL(string: "https://example.net/band")!
|
|
let params = ["email": "fatmike@example.net", "password": "LinoleumSupportsMyHead"]
|
|
let request = HTTPRequest.postJSON(url, body: params)
|
|
let urlRequest = try RequestBuilder.build(request: request)
|
|
_ = urlRequest
|
|
}
|
|
|
|
func moreCodableExamples() throws {
|
|
struct Artist: Codable {
|
|
let name: String
|
|
let email: String
|
|
let genre: String
|
|
}
|
|
|
|
let url = URL(string: "https://beats.example.net/artists")!
|
|
let artist = Artist(name: "Trent Reznor", email: "trent@example.net", genre: "Industrial")
|
|
|
|
// POST with Codable body
|
|
let postRequest = try HTTPRequest.post(url, body: artist)
|
|
|
|
// PUT with Codable body
|
|
let putRequest = try HTTPRequest.put(url, body: artist)
|
|
|
|
// PATCH with Codable body
|
|
let patchRequest = try HTTPRequest.patch(url, body: artist)
|
|
|
|
// Custom encoder for different JSON formatting
|
|
let encoder = JSONEncoder()
|
|
encoder.keyEncodingStrategy = .convertToSnakeCase
|
|
let customRequest = try HTTPRequest.post(url, body: artist, encoder: encoder)
|
|
|
|
_ = postRequest
|
|
_ = putRequest
|
|
_ = patchRequest
|
|
_ = customRequest
|
|
}
|
|
|
|
func formEncoderExample() {
|
|
let body = FormEncoder.encode(["email": "trent@example.net", "password": "CloserToGod"])
|
|
_ = body
|
|
}
|
|
|
|
func multipartFormExample() throws {
|
|
let avatarData = Data("fake image data".utf8) // Simplified for compilation
|
|
let encoder = MultipartFormEncoder()
|
|
let body = try encoder.encodeData(parts: [
|
|
.text("chali@example.net", name: "email"),
|
|
.text("QualityControl", name: "password"),
|
|
.data(avatarData, name: "avatar", type: "image/jpeg", filename: "avatar.jpg"),
|
|
])
|
|
_ = body
|
|
}
|
|
|
|
func completeExample() async throws {
|
|
struct ArtistProfile: Codable {
|
|
let name: String
|
|
let email: String
|
|
let genre: String
|
|
}
|
|
|
|
struct UpdateProfileRequest: Codable {
|
|
let name: String
|
|
let email: String
|
|
let genre: String
|
|
}
|
|
|
|
func updateProfile(name: String, email: String, genre: String) async throws -> ArtistProfile {
|
|
let url = URL(string: "https://beats.example.net/profile")!
|
|
let updateRequest = UpdateProfileRequest(name: name, email: email, genre: genre)
|
|
|
|
// Use Codable body instead of dictionary
|
|
let request = try HTTPRequest.put(url, body: updateRequest)
|
|
|
|
// URLSession extension handles status checking and JSON decoding
|
|
return try await URLSession.shared.perform(request)
|
|
}
|
|
|
|
// For varied data structures, dictionaries are still available as an escape hatch:
|
|
func updateProfileWithDictionary(fields: [String: String]) async throws -> ArtistProfile {
|
|
let url = URL(string: "https://beats.example.net/profile")!
|
|
let request = HTTPRequest.putJSON(url, body: fields)
|
|
return try await URLSession.shared.perform(request)
|
|
}
|
|
|
|
_ = updateProfile
|
|
_ = updateProfileWithDictionary
|
|
}
|
|
|
|
func httpResponseExample() throws {
|
|
let url = URL(string: "https://example.net/test")!
|
|
let urlRequest = URLRequest(url: url)
|
|
|
|
let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
|
|
let httpResponse = HTTPResponse(response: response, data: data, error: error)
|
|
|
|
switch httpResponse {
|
|
case let .success(httpURLResponse, data):
|
|
print("Success: \(httpURLResponse.statusCode)")
|
|
if let data = data {
|
|
print("Response: \(String(data: data, encoding: .utf8) ?? "")")
|
|
}
|
|
case let .failure(error, httpURLResponse, _):
|
|
print("Failed: \(error)")
|
|
if let httpURLResponse = httpURLResponse {
|
|
print("Status: \(httpURLResponse.statusCode)")
|
|
}
|
|
}
|
|
}
|
|
_ = task
|
|
}
|
|
|
|
func multipartFileStreamingExample() throws {
|
|
let encoder = MultipartFormEncoder()
|
|
let avatarData = Data("fake image data".utf8)
|
|
|
|
let body = try encoder.encodeFile(parts: [
|
|
.text("chali@example.net", name: "email"),
|
|
.text("QualityControl", name: "password"),
|
|
.data(avatarData, name: "avatar", type: "image/jpeg", filename: "avatar.jpg"),
|
|
])
|
|
defer { _ = body.cleanup() }
|
|
|
|
var request = URLRequest(url: URL(string: "https://example.net/accounts")!)
|
|
request.httpMethod = "POST"
|
|
request.httpBodyStream = InputStream(url: body.url)
|
|
request.addValue(body.contentType, forHTTPHeaderField: "Content-Type")
|
|
request.addValue("\(body.contentLength)", forHTTPHeaderField: "Content-Length")
|
|
|
|
_ = request
|
|
}
|
|
|
|
func errorHandlingExample() throws {
|
|
func handleErrors() async {
|
|
do {
|
|
let url = URL(string: "https://example.net/test")!
|
|
let request = HTTPRequest.get(url)
|
|
let response: [String] = try await URLSession.shared.perform(request)
|
|
|
|
_ = response
|
|
} catch let httpError as HTTPError {
|
|
switch httpError {
|
|
case let .failure(statusCode, data, _):
|
|
print("HTTP \(statusCode) error: \(String(data: data, encoding: .utf8) ?? "No body")")
|
|
case .invalidResponse:
|
|
print("Invalid response from server")
|
|
}
|
|
} catch is DecodingError {
|
|
print("Failed to decode response JSON")
|
|
} catch {
|
|
print("Network error: \(error)")
|
|
}
|
|
}
|
|
|
|
_ = handleErrors
|
|
}
|
|
|
|
func migrationGuideExamples() throws {
|
|
let url = URL(string: "https://example.net/test")!
|
|
|
|
// Explicit methods for different encodings
|
|
let jsonRequest = HTTPRequest.postJSON(url, body: ["key": "value"])
|
|
let formRequest = HTTPRequest.putForm(url, parameters: ["key": "value"])
|
|
|
|
// Multipart convenience methods
|
|
let multipartRequest = HTTPRequest.postMultipart(url, parts: [.text("value", name: "field")])
|
|
|
|
// Codable support
|
|
struct Artist: Codable {
|
|
let name: String
|
|
let genre: String
|
|
}
|
|
let codableRequest = try HTTPRequest.post(url, body: Artist(name: "Trent Reznor", genre: "Industrial"))
|
|
|
|
_ = jsonRequest
|
|
_ = formRequest
|
|
_ = multipartRequest
|
|
_ = codableRequest
|
|
}
|
|
|
|
func additionalHTTPRequestExamples() throws {
|
|
// Examples from HTTPRequest documentation
|
|
|
|
// GET request with query parameters
|
|
let getRequest = HTTPRequest.get(
|
|
URL(string: "https://api.example.net/users")!,
|
|
parameters: ["page": "1", "limit": "10"]
|
|
)
|
|
|
|
// POST with JSON body
|
|
let jsonRequest = HTTPRequest.postJSON(
|
|
URL(string: "https://api.example.net/users")!,
|
|
body: ["name": "Chali 2na", "email": "chali@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
|
|
let uploadURL = URL(string: "https://api.example.net/upload")!
|
|
let imageData = Data("fake image".utf8)
|
|
let multipartRequest = HTTPRequest.postMultipart(uploadURL, parts: [
|
|
.text("Trent Reznor", name: "name"),
|
|
.data(imageData, name: "avatar", type: "image/jpeg", filename: "avatar.jpg")
|
|
])
|
|
|
|
// File streaming for large request bodies
|
|
let tempFile = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("test.txt")
|
|
try "test content".write(to: tempFile, atomically: true, encoding: .utf8)
|
|
let fileRequest = HTTPRequest.postFile(
|
|
URL(string: "https://api.example.net/upload")!,
|
|
fileURL: tempFile
|
|
)
|
|
|
|
// Custom content types like XML
|
|
let xmlData = "<request><artist>Nine Inch Nails</artist></request>".data(using: .utf8)!
|
|
let xmlRequest = HTTPRequest.post(
|
|
URL(string: "https://api.example.net/music")!,
|
|
data: xmlData,
|
|
contentType: .xml
|
|
)
|
|
|
|
_ = getRequest
|
|
_ = jsonRequest
|
|
_ = deleteRequest
|
|
_ = multipartRequest
|
|
_ = fileRequest
|
|
_ = xmlRequest
|
|
}
|