Skip to content
Merged
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
135 changes: 135 additions & 0 deletions Sources/SwiftRestRequests/Documentation.docc/Articles/Configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Configuring REST Requests

Learn how to configure REST requests using RestOptions.

## Overview

``RestOptions`` provides a flexible way to customize REST API requests in SwiftRestRequests.

## Common Configurations

### Basic Options

```swift
var options = RestOptions()

// Set timeout interval
options.timeoutInterval = 30

// Add custom headers
options.headers = [
"Authorization": "Bearer token",
"Api-Version": "2.0"
]

// Configure expected status codes
options.expectedStatusCodes = [200, 201, 204]

let api = RestApiCaller(options: options)
```

### Query Parameters

```swift
var options = RestOptions()
options.queryParameters = [
"page": "1",
"limit": "10",
"sort": "desc"
]

// Results in URL: https://api.example.com/users?page=1&limit=10&sort=desc
let users: [User] = try await api.get("https://api.example.com/users",
options: options)
```

### Content Types

```swift
var options = RestOptions()
options.acceptedMimeTypes = ["application/json", "application/problem+json"]
```

### Security Configuration

```swift
var options = RestOptions()

// Configure certificate pinning
let certificatePath = Bundle.main.path(forResource: "server-cert", ofType: "der")!
options.serverPinning = CertificateCAPinning(certificatePath: certificatePath)

// Configure TLS
options.tlsConfiguration = ...
```

## Advanced Usage

### Per-Request Options

```swift
let globalOptions = RestOptions()
globalOptions.headers = ["Api-Version": "2.0"]

let api = RestApiCaller(options: globalOptions)

// Override options for specific request
var requestOptions = RestOptions()
requestOptions.headers = ["Authorization": "Bearer special-token"]
requestOptions.timeoutInterval = 60

let response = try await api.post("https://api.example.com/data",
body: payload,
options: requestOptions)
```

### Combining Options

```swift
extension RestOptions {
static func combine(_ options: RestOptions...) -> RestOptions {
var combined = RestOptions()

for option in options {
// Merge headers
combined.headers.merge(option.headers) { $1 }

// Merge query parameters
combined.queryParameters.merge(option.queryParameters) { $1 }

// Use latest non-nil values
if option.timeoutInterval != nil {
combined.timeoutInterval = option.timeoutInterval
}
// ... handle other properties
}

return combined
}
}

// Usage
let baseOptions = RestOptions()
let authOptions = RestOptions(headers: ["Authorization": "Bearer token"])
let customOptions = RestOptions(timeoutInterval: 30)

let combined = RestOptions.combine(baseOptions, authOptions, customOptions)
```

## Topics

### Configuration
- ``RestOptions/headers``
- ``RestOptions/queryParameters``
- ``RestOptions/timeoutInterval``
- ``RestOptions/expectedStatusCodes``
- ``RestOptions/acceptedMimeTypes``

### Security
- ``RestOptions/serverPinning``
- ``RestOptions/tlsConfiguration``

### Related Types
- ``RestApiCaller``
- ``CertificateCAPinning``
- ``PublicKeyServerPinning``
159 changes: 159 additions & 0 deletions Sources/SwiftRestRequests/Documentation.docc/Articles/ErrorHandling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# Error Handling Guide

Learn how to handle errors effectively in SwiftRestRequests.

## Overview

SwiftRestRequests provides comprehensive error handling through the ``RestError`` type. This guide explains common error scenarios and how to handle them effectively.

## Common Error Types

### Network Errors

```swift
do {
let response = try await api.get("https://api.example.com/data")
} catch RestError.badResponse(let response, let data) {
print("Invalid response: \(response)")
// Handle invalid response format
} catch RestError.networkError(let error) {
// Handle network connectivity issues
if let urlError = error as? URLError {
switch urlError.code {
case .notConnectedToInternet:
// Handle no internet connection
case .timedOut:
// Handle request timeout
default:
// Handle other network errors
}
}
}
```

### Data Processing Errors

```swift
do {
let item: Item = try await api.get("https://api.example.com/items/1")
} catch RestError.malformedResponse(let response, let data, let error) {
if let decodingError = error as? DecodingError {
switch decodingError {
case .keyNotFound(let key, _):
print("Missing required field: \(key)")
case .typeMismatch(_, let context):
print("Invalid data type at: \(context.codingPath)")
default:
print("Other decoding error: \(decodingError)")
}
}
}
```

### API Errors

```swift
do {
let response = try await api.post("https://api.example.com/items", body: newItem)
} catch RestError.failedRestCall(let response, let status, let error) {
switch status {
case .unauthorized:
// Handle authentication failure
case .notFound:
// Handle resource not found
case .tooManyRequests:
// Handle rate limiting
default:
// Handle other API errors
}
}
```

## Error Recovery Strategies

### Retry Logic

```swift
func retryableRequest<T: Decodable>(maxAttempts: Int = 3) async throws -> T {
var attempts = 0

while attempts < maxAttempts {
do {
return try await api.get("https://api.example.com/data")
} catch RestError.networkError(let error) {
attempts += 1
if attempts == maxAttempts { throw error }
try await Task.sleep(nanoseconds: UInt64(pow(2.0, Double(attempts))) * 1_000_000_000)
}
}

throw RestError.networkError(NSError(domain: "", code: -1))
}
```

### Graceful Degradation

```swift
func fetchUserData() async throws -> User {
do {
// Try to fetch full user data
return try await api.get("https://api.example.com/users/1")
} catch RestError.failedRestCall(_, .notFound, _) {
// Fall back to cached data
return try await getCachedUser()
} catch RestError.networkError {
// Fall back to offline mode
return try await getOfflineUser()
}
}
```

## Best Practices

### Custom Error Handling

```swift
extension RestError {
var isRetryable: Bool {
switch self {
case .networkError(let error):
return (error as? URLError)?.code != .cancelled
case .failedRestCall(_, let status, _):
return status.rawValue >= 500
default:
return false
}
}
}
```

### Error Logging

```swift
class ErrorLogger {
static func log(_ error: RestError) {
switch error {
case .malformedResponse(let response, let data, let error):
print("""
Failed to process response:
URL: \(response.url?.absoluteString ?? "unknown")
Status: \(response.statusCode)
Error: \(error)
Data: \(String(data: data, encoding: .utf8) ?? "invalid data")
""")
// Handle other cases...
}
}
}
```

## Topics

### Error Types
- ``RestError``
- ``HTTPStatusCode``

### Related
- ``RestApiCaller``
- ``RestOptions``
- ``LogNetworkInterceptor``
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Getting Started

@Metadata {
@Title("Getting Started")
}

Follow these steps to integrate ``SwiftRestRequests`` into a Swift Package or Xcode project and perform your
first request.

## Add the package dependency

Use Swift Package Manager to depend on the library:

```swift
.package(url: "https://github.com/tkausch/SwiftRestRequests", from: "1.6.3")
```

Then add ``SwiftRestRequests`` to your target dependencies. Xcode users can add the package through
**File → Add Packages…** with the same URL.

## Configure a client

Subclass ``RestApiCaller`` (or instantiate it directly) to describe your REST API in a strongly typed way:

```swift
import SwiftRestRequests

final class HttpBinClient: RestApiCaller {
func status204() async throws -> Int {
try await get(at: "status/204")
}

func getEcho() async throws -> (HttpBinResponse?, Int) {
try await get(HttpBinResponse.self, at: "get")
}
}

struct HttpBinResponse: Decodable {
let url: String
let origin: String
}

let client = HttpBinClient(baseUrl: URL(string: "https://httpbin.org")!)
let (response, status) = try await client.getEcho()
```

`RestApiCaller` automatically applies default headers, validates the HTTP status, decodes JSON payloads
with ``DecodableDeserializer``, and throws ``RestError`` when something goes wrong.

## Customize each request

``RestOptions`` lets you override headers, query parameters, or status expectations without reconfiguring
your client:

```swift
var options = RestOptions()
options.httpHeaders = ["X-Test": "demo"]
options.expectedStatusCodes = [200, 204]

let status = try await client.delete(at: "resource/42", options: options)
```

Use `headerGenerator` or register a ``URLRequestInterceptor`` subclass to insert shared headers, log
traffic, inject authentication, or perform response inspection:

```swift
client.registerRequestInterceptor(LogNetworkInterceptor(enableNetworkTracing: true))
```

## Handle errors

All failures surface as ``RestError``. Switch over the cases to provide better feedback or retry logic:

```swift
catch let error as RestError {
switch error {
case .invalidMimeType(let mime):
print("Unexpected MIME type: \(mime ?? "none")")
case .failedRestCall(_, let status, let payload):
print("Server rejected the call with status \(status). Payload: \(String(describing: payload))")
default:
print("Unhandled REST error: \(error)")
}
}
```
Loading