Osiris/Tests/OsirisTests/ReadmeExampleTests.swift
Sami Samhuri d2576b729e
Add Codable support and overhaul the API
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
2025-06-23 23:55:55 -04:00

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
}