An easy to use HTTP networking library for Swift with built-in JSON encoding/decoding, comprehensive error handling, and powerful testing capabilities.
- Automatic Coding: Automatically encodes requests and decodes responses
- Result-based: Uses Swift’s
Resulttype for clean error handling - Cache: Ready-to-use with
URLCache - Flexible: Supports all HTTP methods (GET, POST, PUT, PATCH, DELETE)
- Configurable: Custom headers, query parameters, caching, and more
- Testable: Built-in mocking and stubbing for reliable tests
- Lightweight: ~2MB and 0 dependancies, now and forever.
Add Mercury to your project with Swift Package Manager:
dependencies: [
.package(url: "https://github.com/joshgallantt/Mercury.git", from: "2.1.1")
]Then add "Mercury" to your targets:
.target(
name: "YourApp",
dependencies: ["Mercury"]
),
.testTarget(
name: "YourAppTests",
dependencies: ["Mercury", "MercuryTesting"]
)import Mercury
let client = Mercury(host: "https://accounts.yourApp.com")
struct User: Decodable {
let id: Int
let name: String
let email: String
}
let result = await client.get(
path: "/users/123",
decodeTo: User.self
)
switch result {
case .success(let success):
print("Got user: \(success.data.name)")
// Console: Got user: John Doe
case .failure(let failure):
print("Request failed: \(failure)")
/*
// Console example outputs:
Request failed: Decoding failed in 'User' for key 'email'
Request failed: 401 Unauthorized: Invalid API token
Request failed: 404 Not Found
Request failed: Transport error: Lost Connection
*/
}struct CreateUserRequest: Encodable {
let name: String
let email: String
}
struct CreateUserResponse: Decodable {
let id: Int
let name: String
let email: String
let createdAt: String
}
let newUser = CreateUserRequest(name: "John Doe", email: "[email protected]")
let result = await client.post(
path: "/users",
body: newUser,
decodeTo: CreateUserResponse.self
)let client = Mercury(
host: "https://api.example.com:8080/v1",
defaultHeaders: [
"Accept": "application/json",
"Authorization": "Bearer your-token"
],
defaultCachePolicy: .reloadIgnoringLocalCacheData,
cache: .isolated(
memorySize: 4_000_000, // 4MB in-memory
diskSize: 10_000_000 // 10MB disk
)
)You can decode deeply nested objects just by specifying the type:
struct UserProfile: Decodable {
let user: User
let preferences: UserPreferences
let addresses: [Address]
}let result = await client.get(
path: "/users/123/profile",
decodeTo: UserProfile.self
)Decode into any object:
struct User: Decodable {name: String}
let result = await client.get(
path: "/users/123",
decodeTo: User.self
)Or simple text:
let result = await client.get(
path: "/health",
decodeTo: String.self
)No decode type? No Problem, will return binary Data() by default:
let result = await client.post(
path: "/health",
body: newUser
)You can override headers, add new ones, or specify a custom cache policy for each request:
let result = await client.get(
path: "/users/123",
headers: [
"X-Custom-Header": "custom-value",
"Accept-Language": "en-US"
],
query: [
"include": "profile,preferences",
"format": "detailed"
],
fragment: "section",
cachePolicy: .reloadIgnoringLocalCacheData,
decodeTo: User.self
)Mercury supports two caching strategies:
.shared(default): usesURLCache.shared.isolated: your own limits per client
Example:
let client = Mercury(
host: "https://api.example.com",
cache: .isolated(memorySize: 4_000_000, diskSize: 20_000_000)
)
client.clearCache() // Clears this client’s cache onlyTo clear all shared cache:
Mercury.clearSharedURLCache()Warning
Mercury.clearSharedURLCache() clears the global shared URLCache for your process, this includes any URLSession cache outside of Mercury or its clients.
Pesky decoding errors? Not anymore, see exatly which key you forgot to make optional:
Simple error handling:
switch result {
case .success(let success):
print("Got user: \(success.data.name)")
// Console: Got user: John Doe
case .failure(let failure):
print("Request failed: \(failure)")
/*
// Example outputs:
Request failed: Decoding failed in 'User' for key 'email'
Request failed: 401 Unauthorized: Invalid API token
Request failed: 404 Not Found
Request failed: Transport error: Lost Connection
*/
}Handle specific errors if you need more control:
switch failure.error {
case .invalidURL:
print("Invalid URL configuration")
case .server(let statusCode, let data):
print("Server error: \(statusCode)")
case .invalidResponse:
print("Invalid response from server")
case .transport(let error):
print("Network error: \(error.localizedDescription)")
case .encoding(let error):
print("Encoding failed: \(error)")
case .decoding(let namespace, let key, let error):
print("Failed to decode \(namespace).\(key): \(error)")
}Mercury ships with a mock client for robust, fully isolated unit tests.
import XCTest
import Mercury
import MercuryTesting
final class UserServiceTests: XCTestCase {
private var mockClient: MockMercury!
private var userService: UserService!
override func setUp() {
super.setUp()
mockClient = MockMercury()
userService = UserService(client: mockClient)
}
override func tearDown() {
mockClient.reset()
mockClient = nil
userService = nil
super.tearDown()
}
}func test_givenValidUserId_whenFetchUser_thenReturnsUser() async {
// Given
let expectedUser = User(id: 123, name: "John Doe", email: "[email protected]")
mockClient.stubGet(path: "/users/123", response: expectedUser)
// When
let user = await userService.fetchUser(id: 123)
// Then
XCTAssertEqual(user?.id, 123)
XCTAssertEqual(user?.name, "John Doe")
XCTAssertTrue(mockClient.wasCalled(method: .GET, path: "/users/123"))
}func test_givenServerError_whenFetchUser_thenReturnsNil() async {
// Given
mockClient.stubFailure(
method: .GET,
path: "/users/123",
error: .server(statusCode: 404, data: nil),
decodeTo: User.self
)
// When
let user = await userService.fetchUser(id: 123)
// Then
XCTAssertNil(user)
}func test_givenUserId_whenFetchUser_thenMakesCorrectRequest() async {
// Given
let user = User(id: 123, name: "John Doe", email: "[email protected]")
mockClient.stubGet(path: "/users/123", response: user)
// When
_ = await userService.fetchUser(id: 123)
// Then
XCTAssertEqual(mockClient.callCount(for: .GET, path: "/users/123"), 1)
XCTAssertTrue(mockClient.wasCalled(method: .GET, path: "/users/123"))
let calls = mockClient.recordedCalls
XCTAssertEqual(calls.count, 1)
XCTAssertEqual(calls[0].method, .GET)
XCTAssertEqual(calls[0].path, "/users/123")
XCTAssertFalse(calls[0].hasBody)
}Mercury is available under the MIT License. See the LICENSE file for more details.
Made with ❤️ for the Swift community
