TinyNetworking
This package contains a tiny networking library. It provides a struct Endpoint
, which combines a URL request and a way to parse responses for that request. Because Endpoint
is generic over the parse result, it provides a type-safe way to use HTTP endpoints.
Here are some examples:
A Simple Endpoint
This is an endpoint that represents a user's data (note that there are more fields in the JSON, left out for brevity):
struct User: Codable {
var name: String
var location: String?
}
func userInfo(login: String) -> Endpoint<User> {
return Endpoint(json: .get, url: URL(string: "https://api.github.com/users/\(login)")!)
}
let sample = userInfo(login: "objcio")
The code above is just a description of an endpoint, it does not load anything. sample
is a simple struct, which you can inspect (for example, in a unit test).
Here's how you can load an endpoint. The result
is of type Result<User, Error>
.
URLSession.shared.load(endpoint) { result in
print(result)
}
Alternatively, you can use the async/await option.
let result = try await URLSession.shared.load(endpoint)
Authenticated Endpoints
Here's an example of how you can have authenticated endpoints. You initialize the Mailchimp
struct with an API key, and use that to compute an authHeader
. You can then use the authHeader
when you create endpoints.
struct Mailchimp {
let base = URL(string: "https://us7.api.mailchimp.com/3.0/")!
var apiKey = env.mailchimpApiKey
var authHeader: [String: String] {
["Authorization": "Basic " + "anystring:\(apiKey)".base64Encoded]
}
func addContent(for episode: Episode, toCampaign campaignId: String) -> Endpoint<()> {
struct Edit: Codable {
var plain_text: String
var html: String
}
let body = Edit(plain_text: plainText(episode), html: html(episode))
let url = base.appendingPathComponent("campaigns/\(campaignId)/content")
return Endpoint<()>(json: .put, url: url, body: body, headers: authHeader)
}
}
Custom Parsing
The JSON encoding and decoding are added as conditional extensions on top of the Codable infrastructure. However, Endpoint
itself is not at all tied to that. Here's the type of the parsing function:
var parse: (Data?, URLResponse?) -> Result<A, Error>
Having Data
as the input means that you can write our own functionality on top. For example, here's a resource that parses images:
struct ImageError: Error {}
extension Endpoint where A == UIImage {
init(imageURL: URL) {
self = Endpoint(.get, url: imageURL) { data in
Result {
guard let d = data, let i = UIImage(data: d) else { throw ImageError() }
return i
}
}
}
}
You can also write extensions that do custom JSON serialization, or parse XML, or another format.
Testing Endpoints
Because an Endpoint
is a plain struct, it's easy to test synchronously without a network connection. For example, you can test the image endpoint like this:
XCTAssertThrows(try Endpoint(imageURL: someURL).parse(nil, nil).get())
XCTAssertThrows(try Endpoint(imageURL: someURL).parse(invalidData, nil).get())
XCTAssertNoThrow(try Endpoint(imageURL: someURL).parse(validData, nil).get())
Combine
Hsieh Min Che created a library that adds Combine endpoints to this library: https://github.com/Hsieh-1989/CombinedEndpoint
More Examples
- In the Swift Talk backend, this is used to wrap third-party services.
More Documentation
The design and implementation of this library is covered extensively on Swift Talk. There's a collection with all the relevant episodes: